OSDN Git Service

first commit(1)
authorKei Funagayama <kei.funagayama@hde.co.jp>
Thu, 30 Apr 2009 22:54:15 +0000 (07:54 +0900)
committerKei Funagayama <kei.funagayama@hde.co.jp>
Thu, 30 Apr 2009 22:54:15 +0000 (07:54 +0900)
46 files changed:
AUTHORS [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL [new file with mode: 0644]
INSTALL.ja [new file with mode: 0644]
LICENSE [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README [new file with mode: 0644]
README.ja [new file with mode: 0644]
doc/epydoc.cfg [new file with mode: 0644]
doc/log.conf.example [new file with mode: 0644]
doc/rc.d/init.d/performerd [new file with mode: 0644]
doc/rc.d/init.d/schedulerd [new file with mode: 0644]
doc/rc.d/init.d/silhouetted [new file with mode: 0644]
doc/redhat.spec [new file with mode: 0644]
doc/silhouette.conf.example [new file with mode: 0644]
doc/sysconfig/silhouetted [new file with mode: 0644]
doc/whitelist.conf.example [new file with mode: 0644]
example/dummy.py [new file with mode: 0644]
example/insert_dummy.py [new file with mode: 0644]
example/sendmail.py [new file with mode: 0644]
example/test_failure.py [new file with mode: 0644]
example/test_success.py [new file with mode: 0644]
pysilhouette/__init__.py [new file with mode: 0644]
pysilhouette/command.py [new file with mode: 0644]
pysilhouette/daemon.py [new file with mode: 0644]
pysilhouette/db/__init__.py [new file with mode: 0644]
pysilhouette/db/access.py [new file with mode: 0644]
pysilhouette/db/model.py [new file with mode: 0644]
pysilhouette/log.py [new file with mode: 0644]
pysilhouette/performer.py [new file with mode: 0644]
pysilhouette/prep.py [new file with mode: 0644]
pysilhouette/scheduler.py [new file with mode: 0644]
pysilhouette/silhouette.py [new file with mode: 0644]
pysilhouette/tests/__init__.py [new file with mode: 0644]
pysilhouette/tests/suite.py [new file with mode: 0644]
pysilhouette/tests/testprep.py [new file with mode: 0644]
pysilhouette/tests/testutil.py [new file with mode: 0644]
pysilhouette/tests/testworker.py [new file with mode: 0644]
pysilhouette/uniqkey.py [new file with mode: 0644]
pysilhouette/util.py [new file with mode: 0644]
pysilhouette/worker.py [new file with mode: 0644]
setup.cfg [new file with mode: 0644]
setup.py [new file with mode: 0644]
tool/cleanupdb.py [new file with mode: 0644]
tool/epydoc.sh [new file with mode: 0644]
tool/setdummy.py [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..a838b47
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,7 @@
+        Pysilhouette Authors
+        ====================
+
+The pysilhouette project was initiated by:
+    Kei Funagayama       <kei@karesansui-project.info>
+    Junichi Shinohara    <junichi@karesansui-project.info>
+    Kazuya Hayashi       <kazuya@karesansui-project.info>
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..14e0821
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,4 @@
+Sat May 28 00:00:00 +0900 2009 Kei Funagayama <kei@karesansui-project.info>
+
+    Version 0.6-1 Release 
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..6df4bb0
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,65 @@
+Installing Pysilhouette
+==========================
+
+Copyright (C) 2009 HDE, Inc.
+
+Redistributing, copying, modifying of this file is granted with no restriction.
+
+Basic Installation
+================================================================================
+
+Pysilhouette is 100% pure Python so it does not require compling.
+
+using RPM:
+    # rpm -ivh pysilhouette-xxxx.rpm
+
+using easy_install:
+    # easy_install pysilhouette
+
+Start Up Command/Options
+================================================================================
+
+    # python silhouette.py --help
+    usage: silhouette.py [options]
+
+    options:
+      --version             show program's version number and exit
+      -h, --help            show this help message and exit
+      -c CONFIG, --config=CONFIG
+                            configuration file
+      -d, --daemon          Daemon startup
+      -v, --verbose         Has not been used.
+      -p PIDFILE, --pidfile=PIDFILE
+                            process file path
+      -k, --uniqkey         show unique key
+    
+
+    - Start up in foreground:
+        # python silhouette.py --config=silhouette.conf
+
+    - Start up in background:
+        # python silhouette.py --config=silhouette.conf --pidfile=/var/run/silhouetted.pid
+
+    - Display the unique key of the server:
+        # python silhouette.py --config=silhouette.conf --uniqkey
+
+Database Settings/Initialization
+================================================================================
+
+silhouette.conf set, run the following command.
+    # python tool/cleanupdb.py --config=silhouette.conf
+
+
+Configuration Files
+================================================================================
+    silhouette.conf.example   -> silhouette.conf (needs rename)
+    whitelist.conf.example    -> whitelist.conf (needs rename)
+    log.conf.example          -> log.conf (needs rename)
+
+
+#TODO
+Configuration
+Deploying Init Scripts
+Start/Stop using Init Scripts
+Configuration Details
+How to Register Jobs
diff --git a/INSTALL.ja b/INSTALL.ja
new file mode 100644 (file)
index 0000000..d79ed78
--- /dev/null
@@ -0,0 +1,65 @@
+Pysilhouetteのインストール
+==========================
+
+Copyright (C) 2009 HDE, Inc.
+
+このファイルは、無制限にコピーし再配布が可能です。また、配布して変更も可能です。
+
+基本的なインストール
+================================================================================
+
+100% Pure Pythonで構成されたソフトウェアであるためコンパイルは必要ありません。
+
+RPMからのインストール
+    # rpm -ivh pysilhouette-xxxx.rpm
+
+EASY_INSTALLからのインストール
+    # easy_install pysilhouette
+
+起動コマンド/オプション
+================================================================================
+
+    # python silhouette.py --help
+    usage: silhouette.py [options]
+
+    options:
+      --version             show program's version number and exit
+      -h, --help            show this help message and exit
+      -c CONFIG, --config=CONFIG
+                            configuration file
+      -d, --daemon          Daemon startup
+      -v, --verbose         Has not been used.
+      -p PIDFILE, --pidfile=PIDFILE
+                            process file path
+      -k, --uniqkey         show unique key
+    
+
+    - フォアグラウンドで起動する。
+        # python silhouette.py --config=silhouette.conf
+
+    - バックグラウンドで起動する。
+        # python silhouette.py --config=silhouette.conf --pidfile=/var/run/silhouetted.pid
+
+    - 起動するサーバーのユニークキーを調べる
+        # python silhouette.py --config=silhouette.conf --uniqkey
+
+データベースの設定/初期化
+================================================================================
+
+silhouette.confを設定し、以下のコマンドを実行します。
+    # python tool/cleanupdb.py --config=silhouette.conf
+
+
+設定ファイル一覧
+================================================================================
+    silhouette.conf.example   -> silhouette.conf(rename)
+    whitelist.conf.example    -> whitelist.conf(rename)
+    log.conf.example          -> log.conf(rename)
+
+
+#TODO
+設定ファイルの編集
+起動スクリプトの設置
+起動スクリプトでの起動停止方法
+設定ファイルの各項目の説明
+JOBの登録方法の説明
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..26d268e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 HDE, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..022b69f
--- /dev/null
@@ -0,0 +1,5 @@
+include MANIFEST.in
+include LICENSE
+recursive-include doc *
+recursive-include tool *
+recursive-include example *
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..6d3a37b
--- /dev/null
+++ b/README
@@ -0,0 +1,132 @@
+Abstract/Features
+================================================================================
+Pysilhouette is a 100% pure Python daemon which executes background job commands
+queued in database. Comes with an easy web-based administration interface.
+
+Install
+================================================================================
+See 'INSTALL'.
+
+
+License/Copying
+================================================================================
+
+Copyright (c) 2009 HDE, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+Packages Pysilhouette depends on
+================================================================================
+
+Python
+SQLAlchemy
+
+Downloads for each DBAPI at the time of this writing are as follows:
+
+    * Postgres: psycopg2
+    * SQLite: pysqlite
+    * MySQL: MySQLDB
+    * Oracle: cx_Oracle
+    * MS-SQL, MSAccess: pyodbc (recommended) adodbapi pymssql
+    * Firebird: kinterbasdb
+    * Informix: informixdb
+    * DB2/Informix IDS: ibm-db
+
+
+Directory Structure
+================================================================================
+.
+|-- AUTHORS 
+|-- ChangeLog
+|-- INSTALL
+|-- INSTALL.ja
+|-- LICENSE
+|-- MANIFEST.in x # for distutils packaging
+|-- README
+|-- README.ja # Japanese version of this file.
+|-- doc
+|   |-- epydoc.cfg # Configuration file for epydoc.
+|   |-- log.conf.example # Example config file for logging function.
+|   |-- rc.d
+|   |   `-- init.d
+|   |       |-- performerd # init script for the performer daemon
+|   |       |-- schedulerd # init script for the scheduler daemon
+|   |       `-- silhouetted # init script for the watch daemon
+|   |-- redhat.spec # Spec file for RPM building.
+|   |-- silhouette.conf.example # Example config file for Pysilhouette
+|   |-- sysconfig # System config file.
+|   |   `-- silhouetted
+|   |-- whitelist.conf.example # Example config file for whitelist function
+|   `-- wwwpysilhouette # Web interface
+|       |-- config.py
+|       |-- deletejg.py
+|       |-- form.py
+|       |-- getjg.py
+|       |-- index.py
+|       |-- job_delete.py
+|       |-- job_get.py
+|       |-- job_post.py
+|       |-- job_put.py
+|       |-- postjg.py
+|       |-- putjg.py
+|       |-- statjg.py
+|       |-- style.css
+|       |-- util.py
+|       `-- validate.js
+|-- example # Sample programs using pysilhouette.
+|   |-- dummy.py
+|   |-- insert_dummy.py
+|   |-- sendmail.py
+|   |-- test_failure.py
+|   `-- test_success.py
+|-- pysilhouette # Main program.
+|   |-- __init__.py
+|   |-- command.py
+|   |-- daemon.py # Daemonizing function.
+|   |-- db # Database related files.
+|   |   |-- __init__.py
+|   |   |-- access.py # Database operation.
+|   |   `-- model.py # Database table model.
+|   |-- log.py
+|   |-- performer.py # Performer daemon (executes job commands)
+|   |-- prep.py # Initialize functions.
+|   |-- scheduler.py # Scheduler daemon (schedules job command executions)
+|   |-- silhouette.py # Watch daemon (watched performer/scheduler daemons)
+|   |-- tests # Testing related files.
+|   |   |-- __init__.py
+|   |   |-- suite.py
+|   |   |-- testprep.py
+|   |   |-- testutil.py
+|   |   `-- testworker.py
+|   |-- uniqkey.py # Unique key for the instance.
+|   |-- util.py
+|   `-- worker.py
+|-- setup.cfg # Configuration for distutils packaging.
+|-- setup.py # Main command for distutils packaging.
+`-- tool # Tools for development/operation.
+    |-- cleanupdb.py # Initializes database.
+    |-- epydoc.sh # Generates javadoc-like documents.
+    `-- setdummy.py # Sets some dummy job commands.
+
+Acknowledgment
+================================================================================
+SQLAlchemy and webpy people.
+HDE, Inc. and other related members.
+All people in Python community.
diff --git a/README.ja b/README.ja
new file mode 100644 (file)
index 0000000..84b361a
--- /dev/null
+++ b/README.ja
@@ -0,0 +1,136 @@
+概要
+================================================================================
+Pysilhouetteは、システムのバックグラウンドでデータベースに登録されている
+ジョブコマンドを実行するデーモンです。
+100% Pure Pythonで構成されたソフトウェアです。
+Webからの簡易UIも装備されています。
+
+
+インストールについて
+================================================================================
+
+同一フォルダにあるINSTALLを参照してください。
+
+
+著作権/ライセンス
+================================================================================
+
+Copyright (c) 2009 HDE, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+依存パッケージ
+================================================================================
+
+Python
+SQLAlchemy
+
+サポートしているDBAPI一覧
+
+    * Postgres: psycopg2
+    * SQLite: pysqlite
+    * MySQL: MySQLDB
+    * Oracle: cx_Oracle
+    * MS-SQL, MSAccess: pyodbc (recommended) adodbapi pymssql
+    * Firebird: kinterbasdb
+    * DB2/Informix IDS: ibm-db
+
+
+ディレクトリ構成
+================================================================================
+
+.
+|-- AUTHORS # 著作者
+|-- ChangeLog # チェンジログ
+|-- INSTALL # 英語版インストールマニュアル
+|-- INSTALL.ja # 日本語版インストールマニュアル
+|-- LICENSE # ライセンス 
+|-- MANIFEST.in x # distutilsを利用したパッケージングをするのに使用する。
+|-- README # 英語語版インストールマニュアル
+|-- README.ja  # 日本語版インストールマニュアル
+|-- doc # ドキュメントや設定ファイル関連置き場
+|   |-- epydoc.cfg # epydoc設定ファイル
+|   |-- log.conf.example # ログ設定ファイルのテンプレート
+|   |-- rc.d # 起動スクリプト
+|   |   `-- init.d
+|   |       |-- performerd # パフォーマーーデーモンの起動スクリプト
+|   |       |-- schedulerd # スケジューラーデーモンの起動スクリプト
+|   |       `-- silhouetted # 監視デーモンの起動スクリプト
+|   |-- redhat.spec # RPM用のspecファイル
+|   |-- silhouette.conf.example # Pysilhouette設定ファイルのテンプレート
+|   |-- sysconfig # システム設定ファイル
+|   |   `-- silhouetted
+|   |-- whitelist.conf.example # ホワイトリスト設定ファイルのテンプレート
+|   `-- wwwpysilhouette # 簡易WEBインターフェース
+|       |-- config.py
+|       |-- deletejg.py
+|       |-- form.py
+|       |-- getjg.py
+|       |-- index.py
+|       |-- job_delete.py
+|       |-- job_get.py
+|       |-- job_post.py
+|       |-- job_put.py
+|       |-- postjg.py
+|       |-- putjg.py
+|       |-- statjg.py
+|       |-- style.css
+|       |-- util.py
+|       `-- validate.js
+|-- example # サンプルプログラム関連
+|   |-- dummy.py
+|   |-- insert_dummy.py
+|   |-- sendmail.py
+|   |-- test_failure.py
+|   `-- test_success.py
+|-- pysilhouette # プログラム本体
+|   |-- __init__.py
+|   |-- command.py
+|   |-- daemon.py # デーモン化で利用する関数群
+|   |-- db # Database関連
+|   |   |-- __init__.py
+|   |   |-- access.py # Databaseの操作
+|   |   `-- model.py # Databaseのテーブルモデル
+|   |-- log.py
+|   |-- performer.py # パフォーマーデーモン(ジョブコマンドを実行する)
+|   |-- prep.py # 初期処理で利用する関数群
+|   |-- scheduler.py # スケジューラーデーモン(ジョブコマンドの実行のスケジューリング)
+|   |-- silhouette.py # 監視デーモン(パフォーマーデーモン、スケジューラーデーモンの監視)
+|   |-- tests # テスト関連
+|   |   |-- __init__.py
+|   |   |-- suite.py
+|   |   |-- testprep.py
+|   |   |-- testutil.py
+|   |   `-- testworker.py
+|   |-- uniqkey.py # ユニークキー
+|   |-- util.py
+|   `-- worker.py
+|-- setup.cfg # distutilsを利用したパッケージングをするのに使用する設定ファイル。
+|-- setup.py # distutilsを利用したパッケージングをするのに使用する実行ファイル。
+`-- tool # 開発時や運用時に利用するコマンドベースの実行ファイル
+    |-- cleanupdb.py # Databaseを初期化する実行ファイル
+    |-- epydoc.sh # Javadoc風なドキュメントを自動生成する実行ファイル
+    `-- setdummy.py # 複数のダミージョブコマンドを登録する実行ファイル
+
+感謝
+================================================================================
+SQLAlchemy and webpy people.
+HDE, Inc. and other related members.
+All people in Python community.
diff --git a/doc/epydoc.cfg b/doc/epydoc.cfg
new file mode 100644 (file)
index 0000000..32d873b
--- /dev/null
@@ -0,0 +1,32 @@
+[epydoc]
+
+modules: pysilhouette
+output: html
+target: /var/www/html/pysilhouette-doc
+verbosity: 0
+debug: 0
+simple-term: 0
+docformat: epytext
+parse: yes
+introspect: yes
+# It should be one of: 'grouped', 'listed', 'included'.
+inheritance: listed
+private: yes
+imports: yes
+sourcecode: yes
+include-log: no
+name: Karesansui Project
+css: white
+url: https://sourceforge.jp/projects/pysilhouette/
+link: <a href="http://sourceforge.jp/projects/pysilhouette/">Pysilhouette Project</a>
+# "trees.html", "indices.html", or "help.html"
+#top: os.path
+#help: my_helpfile.html
+frames: yes
+separate-classes: no
+# "classtree", "callgraph", "umlclass", "all"
+graph: all
+dotpath: /usr/bin/dot
+#pstat: profile.out
+graph-font: Helvetica
+graph-font-size: 10
diff --git a/doc/log.conf.example b/doc/log.conf.example
new file mode 100644 (file)
index 0000000..102e060
--- /dev/null
@@ -0,0 +1,48 @@
+[loggers]
+keys=root,pysilhouette,sqlalchemy
+
+[handlers]
+keys=default,pysilhouette,sqlalchemy
+
+[formatters]
+keys=default,common
+
+[formatter_default]
+format=[%(asctime)s] [%(levelname)s] %(message)s
+datefmt=%d/%b/%Y:%H:%M:%S
+
+[formatter_common]
+class=logging.Formatter
+format=[%(asctime)s] [%(levelname)s] [%(process)d] [%(name)s] [%(lineno)d] %(message)s
+datefmt=%d/%b/%Y:%H:%M:%S (%Z) 
+
+[handler_default]
+class=StreamHandler
+formatter=default
+args=(sys.stdout,)
+
+[handler_pysilhouette]
+class=handlers.RotatingFileHandler
+formatter=common
+args=('/var/log/pysilhouette/application.log', 'a', (5 *1024 *1024), 5)
+
+[handler_sqlalchemy]
+class=handlers.RotatingFileHandler
+formatter=common
+args=('/var/log/pysilhouette/database.log', 'a', (5 *1024 *1024), 5)
+
+[logger_root]
+level=INFO
+handlers=default
+
+[logger_pysilhouette]
+level=DEBUG
+handlers=pysilhouette
+propagate=0
+qualname=pysilhouette
+
+[logger_sqlalchemy]
+level=DEBUG
+handlers=sqlalchemy
+propagate=0
+qualname=sqlalchemy
diff --git a/doc/rc.d/init.d/performerd b/doc/rc.d/init.d/performerd
new file mode 100644 (file)
index 0000000..51204a0
--- /dev/null
@@ -0,0 +1,78 @@
+#!/bin/bash
+#
+# performerd   The stop script of the child process(performer.py) of the Pysilhouette system.
+#
+# processname: performerd
+# pidfile: /var/run/performerd
+# lockfile: /var/lock/subsys/performerd
+
+source /etc/rc.d/init.d/functions
+
+# Default value
+prog="performer"
+progd="performerd"
+stop_code='2'
+fifo='/tmp/pysilhouette.fifo'
+
+sysconfig="/etc/sysconfig/${progd}"
+
+if [ "x${PYTHON}" = "x" ]; then
+  PYTHON=`which python`
+fi
+
+# Process id file.
+pidfile="/var/run/${progd}.pid"
+
+desc="${progd} (Daemon)"
+
+stop() {
+    echo -n $"Shutting down $desc: "
+    if [ ! -e ${pidfile} ]; then
+        echo "not running..."
+        return 1
+    fi
+    pid=`cat ${pidfile}`
+    if [ "x${pid}" == "x" ]; then
+        echo "not running... - not pid"
+        rm -f ${pidfile}
+        return 1
+    fi
+    [ -w "${fifo}" ] && echo -n $stop_code >> ${fifo}
+    RETVAL=$?
+    if [ $RETVAL -eq 0 ]; then
+        success
+    else
+        failure
+    fi
+    echo
+    return $RETVAL
+}
+
+err_msg="Please execute \"/etc/rc.d/init.d/silhouetted $1\""
+
+case "$1" in
+    start)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    stop)
+        stop
+        ;;
+    restart|reload)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    condrestart)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    status)
+        status ${progd}
+        RETVAL=$?
+        ;;
+    *)
+        echo $"Usage: $0 {stop|status}"
+        RETVAL=1
+esac
+
+exit $RETVAL
diff --git a/doc/rc.d/init.d/schedulerd b/doc/rc.d/init.d/schedulerd
new file mode 100644 (file)
index 0000000..3913797
--- /dev/null
@@ -0,0 +1,77 @@
+#!/bin/bash
+#
+# schedulerd   The stop script of the child process(scheduler.py) of the Pysilhouette system.
+#
+# processname: schedulerd
+# pidfile: /var/run/schedulerd.pid
+# lockfile: /var/lock/subsys/schedulerd
+
+source /etc/rc.d/init.d/functions
+
+# Default value
+prog="scheduler"
+progd="schedulerd"
+
+sysconfig="/etc/sysconfig/${progd}"
+
+if [ "x${PYTHON}" = "x" ]; then
+  PYTHON=`which python`
+fi
+
+# Process id file.
+pidfile="/var/run/${progd}.pid"
+
+desc="${progd} (Daemon)"
+
+stop() {
+    echo -n $"Shutting down $desc: "
+    if [ ! -e ${pidfile} ]; then
+        echo "not running..."
+        return 1
+    fi
+#    pid=`cat ${pidfile}`
+#    if [ "x${pid}" == "x" ]; then
+#        echo "not running... - not pid"
+#        rm -f ${pidfile}
+#        return 1
+#    fi
+    killproc -p ${pidfile} -15
+#    kill ${pid}
+    RETVAL=$?
+    if [ $RETVAL -eq 0 ]; then
+        success
+    else
+        failure
+    fi
+    echo
+    return $RETVAL
+}
+
+err_msg="Please execute \"/etc/rc.d/init.d/silhouetted $1\""
+
+case "$1" in
+    start)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    stop)
+        stop
+        ;;
+    restart|reload)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    condrestart)
+        echo ${err_msg}
+        RETVAL=1
+        ;;
+    status)
+        status ${progd}
+        RETVAL=$?
+        ;;
+    *)
+        echo $"Usage: $0 {stop|status}"
+        RETVAL=1
+esac
+
+exit $RETVAL
diff --git a/doc/rc.d/init.d/silhouetted b/doc/rc.d/init.d/silhouetted
new file mode 100644 (file)
index 0000000..d5e3740
--- /dev/null
@@ -0,0 +1,171 @@
+#!/bin/bash
+#
+# silhouetted  The startup script for the Pysilhouette system.
+#
+# chkconfig: 345 97 03
+# description: Pysilhouette is an application running in the background system.
+#
+# processname: silhouetted
+# config: /etc/sysconfig/silhouetted
+# pidfile: /var/run/silhouetted.pid
+#          /var/run/schedulerd.pid
+#          /var/run/performerd.pid
+# lockfile: /var/lock/subsys/silhouetted
+#           /var/lock/subsys/schedulerd
+#           /var/lock/subsys/performerd
+
+source /etc/rc.d/init.d/functions
+source /etc/sysconfig/network
+
+# For SELinux we need to use 'runuser' not 'su'
+if [ -x /sbin/runuser ]; then
+    SU=runuser
+else
+    SU=su
+fi
+
+# Check that networking is up.
+[ ${NETWORKING} = "no" ] && exit 1
+
+#Default value
+prog="silhouette"
+progd="silhouetted"
+app="pysilhouette"
+sch_progd='schedulerd'
+per_progd='performerd'
+
+sysconfig="/etc/sysconfig/${progd}"
+
+# Read configuration
+[ -r "${sysconfig}" ] && source "${sysconfig}"
+
+if [ "x${PYTHON}" == "x" ]; then
+  PYTHON=`which python`
+fi
+
+# Config file.
+conf="/etc/opt/${app}/${prog}.conf"
+
+# Process id file.
+pidfile="/var/run/${progd}.pid"
+lockfile="/var/lock/subsys/${progd}"
+sch_pidfile="/var/run/${sch_progd}.pid"
+sch_lockfile="/var/lock/subsys/${sch_progd}"
+per_pidfile="/var/run/${per_progd}.pid"
+per_lockfile="/var/lock/subsys/${per_progd}"
+
+# Daemon mode.
+extra_args=""
+if [ "x${DAEMON}" = "xyes" ]; then
+    extra_args=${extra_args}" -d"
+fi
+
+# Debug mode.
+if [ "x${DEBUG}" = "xyes" ]; then
+    extra_args=${extra_args}" -v"
+fi
+
+desc="${progd} (Daemon)"
+
+# options
+CMD_ARGS="-p ${pidfile} -c ${conf} ${extra_args}"
+
+
+start() {
+    echo -n $"Starting $desc: "
+    if [ -e ${pidfile} ]; then
+           echo "already running..."
+           return 1
+    fi
+
+    touch ${pidfile} ${sch_pidfile} ${per_pidfile}
+    chown ${USER}:${GROUP} ${pidfile} ${sch_pidfile} ${per_pidfile}
+    if [ "x${PYTHON_SEARCH_PATH}" != "x" ]; then
+        env="PYTHONPATH=${PYTHON_SEARCH_PATH}:\$PYTHONPATH"
+    fi
+    ${SU} -l ${USER} -c "${env} ${PYTHON} ${PREFIX}/opt/pysilhouette/bin/${prog}.py ${CMD_ARGS}"
+    RETVAL=$?
+    [ ${RETVAL} -eq 0 ] && touch ${lockfile} ${sch_lockfile} ${per_lockfile}
+    [ ${RETVAL} -eq 0 ] && success || failure 
+    echo ""
+    return ${RETVAL} 
+}
+
+silhouetted_stop() {
+    echo -n $"Shutting down $desc: "
+    if [ ! -e ${pidfile} ]; then
+        echo "not running..."
+        return 1
+    fi
+    pid=`cat ${pidfile}`
+    if [ "x${pid}" == "x" ]; then
+        echo "not running... - not pid"
+        rm -f ${pidfile}
+        return 1
+    fi
+    killproc -p ${pidfile} -15
+    echo
+    RETVAL=$?
+    return ${RETVAL}
+}
+
+stop() {
+    silhouetted_stop
+    SIL_RETVAL=$?
+    if [ ${SIL_RETVAL} -eq 0 ]; then
+        rm -f ${lockfile}
+        rm -f ${pidfile}
+    fi  
+    eval "/etc/rc.d/init.d/${sch_progd} stop"
+    SCH_RETVAL=$?
+    if [ ${SCH_RETVAL} -eq 0 ]; then
+        rm -f ${sch_lockfile}
+        rm -f ${sch_pidfile}
+    fi  
+    eval "/etc/rc.d/init.d/${per_progd} stop"
+    PER_RETVAL=$?
+    if [ ${PER_RETVAL} -eq 0 ]; then
+        rm -f ${per_lockfile}
+        rm -f ${per_pidfile}
+    fi  
+    # The return code of the performer demon is the first digit. 
+    # The return code of the scheduler demon is the second digit. 
+    # The return code of the silhouetted demon is the third digit. 
+    # All stop functions return only the exit code of 0(Normal) or 1(Abnormal).
+    RETVAL=`expr ${SIL_RETVAL} \* 100 + ${SCH_RETVAL} \* 10 + ${PER_RETVAL}`
+    return ${RETVAL}
+}
+
+restart() {
+    stop
+    sleep 1
+    start
+}
+
+
+case "$1" in
+    start)
+       start
+       ;;
+    stop)
+       stop
+       ;;
+    restart|reload)
+       restart
+       ;;
+    condrestart)
+       [ -e ${lockfile} ] && restart
+       RETVAL=$?
+       ;;
+    status)
+       status ${progd}
+    eval "/etc/rc.d/init.d/${sch_progd} status"
+    eval "/etc/rc.d/init.d/${per_progd} status"
+       RETVAL=$?
+       ;;
+    *)
+       echo $"Usage: $0 {start|stop|restart|condrestart|status}"
+       RETVAL=1
+esac
+
+exit $RETVAL
diff --git a/doc/redhat.spec b/doc/redhat.spec
new file mode 100644 (file)
index 0000000..3131c31
--- /dev/null
@@ -0,0 +1,134 @@
+%define name pysilhouette
+%define version 0.6
+%define release 1
+%define date %(echo `LANG=C date +%%Y%%m%%d%%H%%M%%S`)
+
+%define _prefix /opt
+
+%define __python $(which python)
+%define __app pysilhouette
+%define __prog silhouette
+%define __progd %{__prog}d
+%define __sysconfdir %{_sysconfdir}/opt/%{__app}
+%define __bindir %{_prefix}/%{__app}/bin
+%define __datadir %{_var}/opt/%{__app}
+%define _defaultdocdir %{_prefix}/%{__app}/share/doc
+%define python_sitelib  %{_prefix}/%{__app}/lib/python
+
+%define _user           pysilhouette
+%define _group          pysilhouette
+%define _uid_min        300
+%define _uid_max        350
+
+Summary: Damon System is an application running in the background.
+Summary(ja): オープンソースのジョブ実行管理アプリケーション
+Name: %{name}
+Version: %{version}
+Release: %{release}.%{date}
+Source0: %{name}-%{version}.tar.gz
+License: MIT/X Consortium License
+Group: System Environment/Daemons
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
+Prefix: %{_prefix}
+BuildArch: noarch
+Vendor: HDE Package Maintainer <info@hde.co.jp>
+Url: http://sourceforge.jp/projects/pysilhouette/
+
+%description
+Pysilhouette is an application running in the background system.
+A system executes the job command registered into the database.
+100% Pure Python.
+
+%prep
+%setup
+
+%build
+python setup.py build
+
+%install
+python setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --home=%{_prefix}/%{__app}
+
+mkdir -p $RPM_BUILD_ROOT%{__sysconfdir}
+mkdir -p $RPM_BUILD_ROOT%{__bindir}
+mkdir -p $RPM_BUILD_ROOT%{__datadir}
+mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
+mkdir -p $RPM_BUILD_ROOT/etc/sysconfig
+mkdir -p $RPM_BUILD_ROOT/var/log/%{__app}/
+
+install -c -m 644 doc/log.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/log.conf.default
+install -c -m 644 doc/log.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/log.conf
+
+install -c -m 644 doc/%{__prog}.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/%{__prog}.conf.default
+install -c -m 644 doc/%{__prog}.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/%{__prog}.conf
+
+install -c -m 644 doc/whitelist.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/whitelist.conf.default
+install -c -m 644 doc/whitelist.conf.default $RPM_BUILD_ROOT%{__sysconfdir}/whitelist.conf
+
+install -c -m 644 doc/rc.d/init.d/* $RPM_BUILD_ROOT%{_initrddir}/
+install -c -m 644 doc/sysconfig/%{__progd} $RPM_BUILD_ROOT/etc/sysconfig/%{__progd}
+
+chmod +x $RPM_BUILD_ROOT%{python_sitelib}/%{__app}/%{__prog}.py
+%{__ln_s} %{python_sitelib}/%{__app}/%{__prog}.py $RPM_BUILD_ROOT%{__bindir}
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%pre
+# Add group
+getent group | %{__grep} "^%{_group}:" >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+  __uid=%{_uid_min}
+  while test ${__uid} -le %{_uid_max}
+  do
+    getent group | %{__grep} "^[^:]*:x:${__uid}:" >/dev/null 2>&1
+    if [ $? -ne 0 ]; then
+      _gid=${__uid}
+      break
+    fi
+    __uid=`expr ${__uid} + 1`
+  done
+  /usr/sbin/groupadd -g ${_gid} -f %{_group}
+fi
+
+# Add user
+getent passwd | %{__grep} "^%{_user}:" >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+  __uid=%{_uid_min}
+  while test ${__uid} -le %{_uid_max}
+  do
+    getent passwd | %{__grep} "^[^:]*:x:${__uid}:" >/dev/null 2>&1
+    if [ $? -ne 0 ]; then
+      _uid=${__uid}
+      break
+    fi
+    __uid=`expr ${__uid} + 1`
+  done
+  /usr/sbin/useradd -c "pysilhouette" -u ${_uid} -g %{_group} -s /bin/false -r %{_user} 2> /dev/null || :
+fi
+
+%postun
+if [ $1 = 0 ]; then
+  /usr/sbin/userdel %{_user} 2> /dev/null || :
+  /usr/sbin/groupdel %{_group} 2> /dev/null || :
+fi
+
+
+%files -f INSTALLED_FILES
+%defattr(-,root,root)
+%doc doc
+%dir %attr(0755, root, root) %{__sysconfdir}
+%attr(0755, root, root) %{_initrddir}/*
+%attr(0644, root, root) %config(noreplace) %{__sysconfdir}/log.conf
+%attr(0644, root, root) %{__sysconfdir}/log.conf.default
+%attr(0644, root, root) %config(noreplace) %{__sysconfdir}/%{__prog}.conf
+%attr(0644, root, root) %{__sysconfdir}/%{__prog}.conf.default
+%attr(0644, root, root) %config(noreplace) %{__sysconfdir}/whitelist.conf
+%attr(0644, root, root) %{__sysconfdir}/whitelist.conf.default
+%attr(0644, root, root) %config(noreplace) /etc/sysconfig/%{__progd}
+%{__bindir}/%{__prog}.py
+%dir %{__datadir}
+%dir /var/log/%{__app}
+
+%changelog
+* Sat May 28 2009 HDE Package Maintainer <info@hde.co.jp> - 0.6-1
+- new version.
diff --git a/doc/silhouette.conf.example b/doc/silhouette.conf.example
new file mode 100644 (file)
index 0000000..34be78e
--- /dev/null
@@ -0,0 +1,73 @@
+##
+# Environment variables
+#
+env.python=/usr/bin/python
+env.sys.log.conf.path=/etc/opt/pysilhouette/log.conf
+# To set a unique key, please.
+#     command : python uniqkey.py
+env.uniqkey=aaaaaaaa-0000-0000-0000-aaaaaaaaaaaa
+
+##
+# deamon
+daemon.stdin=/dev/null
+daemon.stdout=/var/log/pysilhouette/stdout.log
+daemon.stderr=/var/log/pysilhouette/stderr.log
+
+##
+# observer
+observer.target.python=/usr/bin/python
+observer.target.scheduler=/opt/pysilhouette/lib/python/pysilhouette/scheduler.py
+observer.target.performer=/opt/pysilhouette/lib/python/pysilhouette/performer.py
+observer.restart.count=5
+#  - Clear intervals. 0=Infinite
+observer.restart.count.clear.time=300
+#  - Check interval
+observer.check.interval=5
+#  - Output status information
+observer.status.path=/var/opt/pysilhouette/status
+#  - mkfifo
+observer.mkfifo.path=/tmp/pysilhouette.fifo
+observer.mkfifo.start.code=0
+observer.mkfifo.ignore.code=1
+observer.mkfifo.stop.code=2
+observer.mkfifo.user.name=pysilhouette
+observer.mkfifo.group.name=pysilhouette
+observer.mkfifo.perms=0666
+
+##
+# scheduler
+scheduler.interval=10
+
+##
+# job
+job.popen.env.lang=C
+job.popen.timeout=3600
+job.popen.waittime=10
+# 1 or Other
+job.whitelist.flag=1
+job.whitelist.path=/etc/opt/pysilhouette/whitelist.conf
+
+##
+# Database RFC-1738 style URLs.
+# - driver://username:password@host:port/database
+# 
+# postgresql :
+#      database.url=postgres://silhouette:<password>@localhost:5432/silhouette
+#
+# mysql : 
+#      database.url=mysql://localhost/silhouette
+#      or
+#      database.url=mysql://silhouette:<password>@localhost/silhouette
+#
+# oracle
+#   database.url=oracle://scott:tiger@dsn - TNS
+#      database.url=oracle://scott:tiger@127.0.0.1:1521/sidname - host/port/SID
+#
+# sqlite
+#   database.url=sqlite:////absolute/path/to/silhouette.db - absolute path
+#      database.url=sqlite:///relative/path/to/silhouette.db - relative path
+#      database.url=sqlite:// - in memory
+#      database.url=sqlite://:memory: - in memory
+#
+#database.url=sqlite:///:memory:
+database.url=sqlite:////var/opt/pysilhouette/pysilhouette.db
diff --git a/doc/sysconfig/silhouetted b/doc/sysconfig/silhouetted
new file mode 100644 (file)
index 0000000..0f9a74d
--- /dev/null
@@ -0,0 +1,9 @@
+# pysilhouetted Counfigure.
+PREFIX=""
+USER="root"
+GROUP="root"
+PYTHON="/usr/bin/python"
+DAEMON="yes"
+DEBUG="no"
+# PYTHON_SEARCH_PATH={BAR}:{FOO}
+PYTHON_SEARCH_PATH="/opt/hde/lib/python:/opt/pysilhouette/lib/python"
diff --git a/doc/whitelist.conf.example b/doc/whitelist.conf.example
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/example/dummy.py b/example/dummy.py
new file mode 100644 (file)
index 0000000..0937aa9
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+
+from pysilhouette.command import Command
+
+class DummyCommand(Command):
+
+    def process(self):
+        import time
+        for x in xrange(0, 10):
+            time.sleep(1)
+            self.up_progress(10)
+            
+        return True
+if __name__ == '__main__':
+    dc = DummyCommand()
+    sys.exit(dc.run())
diff --git a/example/insert_dummy.py b/example/insert_dummy.py
new file mode 100644 (file)
index 0000000..60ccae4
--- /dev/null
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+from pysilhouette.command import Command
+from pysilhouette.db.model import JobGroup, Job
+
+class DummyCommand(Command):
+
+    def insert_data(self):
+        self.db.get_metadata().drop_all()
+        self.db.get_metadata().create_all()
+
+        jg_name = u'JobGroup-Dummy'
+        jg_ukey = unicode(self.cf['env.uniqkey'], "utf-8")
+        j_name = u'Job-Dummy'
+        j_order = 0
+        j_cmd = unicode( os.path.dirname(__file__) + "/dummy.py", "utf-8")
+        jg = JobGroup(jg_name, jg_ukey)
+        jg.jobs.append(Job(j_name, j_order, j_cmd))
+        self.session.save(jg)
+        self.session.commit()
+
+if __name__ == '__main__':
+    dc = DummyCommand()
+    sys.exit(dc.insert_data())
diff --git a/example/sendmail.py b/example/sendmail.py
new file mode 100644 (file)
index 0000000..89354f2
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# 
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import os
+import smtplib
+from optparse import OptionParser, OptionValueError
+from email.MIMEText import MIMEText
+from email.Utils import formatdate
+from email.Header import Header
+
+
+usage = "%prog [options]"
+version = '%prog 0.1'
+
+charset = 'us-ascii'
+encode = 'utf-8'
+
+def getopts():
+    optp = OptionParser(usage=usage, version=version)
+    optp.add_option('-f', '--from', dest='ofrom', help='Mail From')
+    optp.add_option('-t', '--to', dest='oto', help='RCPT TO')
+    optp.add_option('-s', '--subject', dest='osubject', help='Subject')
+    optp.add_option('-m', '--msg', dest='omsg', help='E-mail message (--bodyfile is the priority.)')
+    optp.add_option('-b', '--bodyfile', dest='obodyfile', help='E-mail files')
+    
+    optp.add_option('-n', '--hostname', dest='ohostname', help='The host name of the SMTP server')
+    optp.add_option('-p', '--port', dest='oport', help='The port of the SMTP server')
+    optp.add_option('-c', '--charset', dest='ocharset', help='Mail character set (Default: us-ascii)')
+    optp.add_option('-e', '--encode', dest='oencode', help='Enter the character code (Default: utf-8)')
+    
+    return optp.parse_args()
+
+def chkopts(opts):
+    if not opts.ofrom:
+        print >>sys.stderr, 'sendmail: --from is required.'
+        return True
+    if not opts.oto:
+        print >>sys.stderr, 'sendmail: --to is required.'
+        return True
+    if not opts.osubject:
+        print >>sys.stderr, 'sendmail: --subject is required.'
+        return True
+    if not opts.omsg and not opts.obodyfile:
+        print >>sys.stderr, 'sendmail: --msg or --bodyfile are required.'
+        return True
+    if not opts.omsg and opts.obodyfile:
+        if not os.path.exists(opts.obodyfile):
+            print >>sys.stderr, 'sendmail: --bodyfile specified in the file does not exist.'
+            return True
+    if not opts.ohostname:
+        print >>sys.stderr, 'sendmail: --hostname is required.'
+        return True
+    if not opts.oport:
+        print >>sys.stderr, 'sendmail: --port is required.'
+        return True
+    if not opts.ocharset:
+        opts.ocharset = charset
+    if not opts.oencode:
+        opts.oencode = encode
+        
+    return False
+
+def sendmail(opts):
+    if opts.obodyfile:
+        fp = open(opts.obodyfile, 'r')
+        body = unicode(fp.read(), opts.oencode).encode(opts.ocharset,'replace')
+    else:
+        body = unicode(opts.omsg, opts.oencode).encode(opts.ocharset,'replace')
+
+    msg = MIMEText(body, 'plain', opts.ocharset)
+    msg['Subject'] = Header(unicode(opts.osubject, opts.oencode), opts.ocharset)
+    msg['From'] = opts.ofrom
+    msg['To'] = opts.oto
+    msg['Date'] = formatdate()
+
+    s = smtplib.SMTP()
+    s.connect(opts.ohostname, opts.oport)
+    s.sendmail(opts.ofrom, [opts.oto], msg.as_string())
+    s.close()
+    
+def main():
+    (opts, args) = getopts()
+    if chkopts(opts):
+        return 1
+    sendmail(opts)
+    
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/example/test_failure.py b/example/test_failure.py
new file mode 100644 (file)
index 0000000..2899b5c
--- /dev/null
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+
+fpath = '/tmp/pysilhouette_job_failure.txt'
+
+if __name__ == '__main__':
+    fp= open(fpath, 'w')
+    fp.write('Failure!!\n')
+    fp.close()
+    try:
+        # os.unlink(fpath)
+        raise Exception('Failure!!')
+    except Exception, e:
+        print >>sys.stderr, 'stderr : %s!!' % e.args
+        sys.exit(1)
diff --git a/example/test_success.py b/example/test_success.py
new file mode 100644 (file)
index 0000000..c905b9b
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+
+fpath = '/tmp/pysilhouette_job_success.txt'
+
+if __name__ == '__main__':
+    fp= open(fpath, 'w')
+    fp.write('Success!!\n')
+    fp.close()
+    os.unlink(fpath)
+    print >>sys.stdout, 'stdout : Success!!'
+    sys.exit(0)
diff --git a/pysilhouette/__init__.py b/pysilhouette/__init__.py
new file mode 100644 (file)
index 0000000..7baa236
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+__version__ = '0.6'
+__release__ = '1'
+__app__ = 'pysilhouette'
+
+class SilhouetteException(StandardError):
+    pass
diff --git a/pysilhouette/command.py b/pysilhouette/command.py
new file mode 100644 (file)
index 0000000..d14ae57
--- /dev/null
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import os
+import logging
+import sys
+import traceback
+
+import pysilhouette.prep
+import pysilhouette.log
+from pysilhouette.util import is_empty
+from pysilhouette.db import Database
+from pysilhouette.db.model import reload_mappers
+from pysilhouette.db.access import \
+     get_progress as dba_get_progress, \
+     up_progress as dba_up_progress
+
+# lib
+def dict2command(cmd, options={}):
+    """ Provides simple method to build up 
+    "/usr/bin/foo --key1=value1 --key2=value2"
+    styled command line.
+    @param cmd: command path to be executed.
+    @type cmd: string
+    @param options: dictionary which contains
+        {key: value} to be transformed into
+        "--key=value". Defaults to {}.
+        If you like to pass a flag styled option(like --flag),
+        just put None into value.
+    @type options: dictionary
+    @return: command line in single string.
+    """
+    if is_empty(cmd):
+        raise CommandException("command not found. - cmd=%s" % cmd)
+
+    ret = ""
+    for x in options.keys():
+        if options[x] is None:
+            ret += "--%s " % x 
+        else:
+            ret += "--%s=%s " % (x, options[x])
+    return "%s %s" % (cmd.strip(), ret.strip())
+
+# public
+class CommandException(Exception):
+    """Command execution error.
+    """
+    pass
+
+class Command:
+    """Command is a class designed to assist implmentation of CLI programs.
+    Expacted to be inherited.
+    """
+
+    cf = None
+    logger = None
+
+    def __init__(self):
+        if os.environ.has_key('PYSILHOUETTE_CONF') is False:
+            print >>sys.stderr, '[Error] "PYSILHOUETTE_CONF" did not exist in the environment.'
+            sys.exit(1)
+            
+        self.cf = pysilhouette.prep.readconf(os.environ['PYSILHOUETTE_CONF'])
+        if self.logger is None:
+            pysilhouette.log.reload_conf(self.cf["env.sys.log.conf.path"])
+            self.logger = logging.getLogger('pysilhouette.command') 
+
+        try:
+            self.db = Database(self.cf['database.url'],
+                          encoding="utf-8",
+                          convert_unicode=True,
+                          assert_unicode=False, # product
+                          #assert_unicode='warn', # dev
+                          echo = True,
+                          echo_pool = True,
+                          )
+
+            reload_mappers(self.db.get_metadata())
+            self.session = self.db.get_session()
+        except Exception, e:
+            print >>sys.stderr, '[Error] Initializing a database error - %s' % str(e.args)
+            self.logger.error('Initializing a database error - %s' % str(e.args))
+            t_logger = logging.getLogger('pysilhouette_traceback')
+            t_logger.error(traceback.format_exc())
+            sys.exit(1)
+
+    def _pre(self):
+        if os.environ.has_key('JOB_ID') is False:
+            self.logger.debug('"JOB_ID" did not exist in the environment.')
+            self.job_id = None
+        else:
+            self.job_id = os.environ['JOB_ID']
+            self.logger.debug('Command JOB_ID=%s' % self.job_id)
+            
+        return True
+            
+    def _post(self):
+        return True
+    
+    def run(self):
+        try:
+            try:
+                try:
+                    if self._pre() is False:
+                        CommandException("Error running in _pre().")
+                    if self.process() is False:
+                        CommandException("Error running in process().")
+                    if self._post() is False:
+                        CommandException("Error running in _post().")
+                         
+                    return 0
+                
+                except CommandException, e:
+                    self.logger.error("Command execution error - %s" % str(e.args))
+                    print >>sys.stderr, _("Command execution error - %s") % str(e.args)
+                    raise
+            except:
+                self.session.rollback()
+                return 1
+        finally:
+            self.session.commit()
+
+    def process(self):
+        raise CommandException('Please use the override.')
+
+    def up_progress(self, r):
+        """ Increments progress counter by r.
+        If the counter reaches 100, then it will be 100 regardless of the value of r.
+        @param r: Amount to increment.
+        @type r: int
+        """
+        if self.job_id is None:
+            self.logger.warn('up_progress called but no job ID is assigned with this object. Ignoring.')
+            return None
+        else:
+            return dba_up_progress(self.session, self.job_id, r)
+        
+    def get_progress(self):
+        """ Returns the current progress counter.
+        @return: Progress counter value on success (0-100). -1 on failure.
+        """
+        if self.job_id is None:
+            self.logger.warn('get_progress called but no job ID is assigned with this object. Ignoring and returning -1.')
+            return -1
+        else:
+            ret = dba_get_progress(self.session, self.job_id)
+            self.session.commit()
+            return ret
+
+if __name__ == '__main__':
+    pass
diff --git a/pysilhouette/daemon.py b/pysilhouette/daemon.py
new file mode 100644 (file)
index 0000000..3f7c19c
--- /dev/null
@@ -0,0 +1,240 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import time
+import os
+import sys
+import math
+import subprocess
+import signal
+import logging
+
+import pysilhouette
+import pysilhouette.log
+from pysilhouette.util import astrftime
+from pysilhouette.util import kill_proc
+
+def observer(opts, cf):    
+    """scheduler and performer manage and monitor.
+    @param opts: command options
+    @type opts: dict(OptionParser)
+    @param cf: Configuration info
+    @type cf: dict
+    @rtype: int
+    @return: exit code
+    """
+    def scheduler():
+        cmd = [cf['observer.target.python'], cf['observer.target.scheduler']]
+        if cmd_args: 
+            cmd.extend(cmd_args)
+        if opts.daemon is True:
+            cmd.extend(['-p', os.path.abspath(os.path.dirname(opts.pidfile)) + '/schedulerd.pid'])
+            
+        logger.debug('scheduler:popen - cmd=%s' % cmd)
+        return subprocess.Popen(args=cmd,
+                                close_fds=True,
+                                env=this_env,
+                                shell=False)
+
+    def performer():
+        cmd = [cf['observer.target.python'], cf['observer.target.performer']]
+        if cmd_args: 
+            cmd.extend(cmd_args)
+        if opts.daemon is True:
+            cmd.extend(['-p', os.path.abspath(os.path.dirname(opts.pidfile)) + '/performerd.pid'])
+
+        logger.debug('performer:popen - cmd=%s' % cmd)
+        return subprocess.Popen(args=cmd,
+                                close_fds=True,
+                                env=this_env,
+                                shell=False)
+
+    def status(count, status, default, force=False):
+        try:
+            if (force is True) or (status != count):
+                status = count
+                fp = open(cf["observer.status.path"], "w")
+                try:
+                    logger.debug("%d/%d" % (count, default))
+                    fp.write("%d/%d" % (count, default))
+                except:
+                    fp.close()
+            else:
+                pass
+            
+        except IOError, ioe:
+            logger.error("Failed to write status. file=%s - %s" \
+                        % (cf["observer.status.path"], str(ioe.args)))
+
+    ##
+    logger = logging.getLogger('pysilhouette.observer')
+
+    # environment
+    this_env = os.environ
+    cmd_args = ['-c', opts.config]
+
+    if opts.verbose is True:
+        cmd_args.append('-v')
+    if opts.daemon is True:
+        cmd_args.append('-d')
+
+    spoint = time.time()
+    
+    default_count = int(cf['observer.restart.count']) # default
+    status_count = default_count # status
+    count = default_count # now
+
+    sd = pf = None
+
+    pf = performer() # start!!
+    logger.info('performer : [start] - pid=%s, count=%s/%s'
+                 % (pf.pid, count, cf['observer.restart.count']))
+    sd = scheduler() # start!!
+    logger.info('scheduler : [start] - pid=%s, count=%s/%s'
+                 % (sd.pid, count, cf['observer.restart.count']))
+
+    status(count, status_count, default_count, True)
+
+    try:
+        while True:
+            if not pf.poll() is None:
+                logger.debug('return code=%d' % pf.returncode)
+                logger.info('performer : [stop] - pid=%s, count=%s/%s'
+                             % (pf.pid, count, cf['observer.restart.count']))
+                pf = performer() # restart
+                count -= 1
+                logger.info('performer : [start] - pid=%s, count=%s/%s'
+                             % (pf.pid, count, cf['observer.restart.count']))
+            else:
+                logger.info('performer [running] - pid=%s, count=%s/%s'
+                             % (pf.pid, count, cf['observer.restart.count']))
+                
+            if not sd.poll() is None:
+                logger.debug('return code=%d' % sd.returncode)
+                logger.info('scheduler : [stop] - pid=%s, count=%s/%s'
+                             % (sd.pid, count, cf['observer.restart.count']))
+                sd = scheduler() # restart
+                count -= 1
+                logger.info('scheduler : [start] - pid=%s, count=%s/%s'
+                                  % (sd.pid, count, cf['observer.restart.count']))
+            else:
+                logger.info('scheduler [running] - pid=%s, count=%s/%s'
+                             % (sd.pid, count, cf['observer.restart.count']))
+    
+            # status output
+            status(count, status_count, default_count, False)
+    
+            if ( 0 < int(cf['observer.restart.count.clear.time']) ) and (count <= 0):
+                epoint = time.time()
+                interval = int(math.ceil(epoint) - math.floor(spoint))
+
+                logger.error('observer restart count reached the value specified in config. Checking interval time.  observer.restart.count=%d interval=%d/%s'
+                             % (cf['observer.restart.count'], interval, cf['observer.restart.count.clear.time']))
+            
+                if interval < int(cf['observer.restart.count.clear.time']):
+                    # Failed 'observer.restart.count' times in 'observer.restart.count.clear.time' seconds.
+                    logger.error('observer restarted %s times in count.clear.time seconds interval. Recognizing as failure. Exiting.'
+                                 % cf['observer.restart.count'])
+                    break
+                else:
+                    # Failed 'observer.restart.count' times in an interval longer than
+                    # 'observer.restart.count.clear.time' seconds. Clearing counter.
+                    spoint = time.time()
+                    count = int(cf['observer.restart.count'])
+                    logger.info('observer restarted %s times, but in not short time. Clearing count. start time %s'
+                                 % (cf['observer.restart.count'], astrftime(spoint)))
+                                      
+            time.sleep(int(cf['observer.check.interval']))
+
+        # -- end while
+        
+    finally:
+        # destory
+        if not sd is None:
+            if kill_proc(sd) is True:
+                logger.info('KILL %d: killing scheduler succeeded.' % sd.pid)
+            else:
+                logger.info('KILL %d: killing scheduler failed.' % sd.pid)
+            
+        if not pf is None:
+            if kill_proc(pf) is True:
+                logger.info('KILL %d: killing performer succeeded.' % pf.pid)
+            else:
+                logger.info('KILL %d: killing performer failed.' % pf.pid)
+
+    return 1
+
+# -- deamon
+def daemonize(stdin, stdout, stderr, pidfile):
+    """The state is changed into daemon.
+    """
+    logger = logging.getLogger('pysilhouette.daemonize')
+
+    try:
+        pid = os.fork()
+        if pid > 0: sys.exit(0)
+    except OSError, e:
+        print >>sys.stderr, 'fork #1 failed: (%d) %s\n' % (e.errno, e.strerror)
+        logger.error('fork #1 failed: (%d) %s\n' % (e.errno, e.strerror))
+        sys.exit(1)
+    os.chdir('/')
+    os.umask(0)
+    os.setsid()
+    try:
+        pid = os.fork()
+        if pid > 0: sys.exit(0)
+    except OSError, e:
+        print >>sys.stderr, 'fork #2 failed: (%d) %s\n' % (e.errno, e.strerror)
+        logger.error('fork #2 failed: (%d) %s\n' % (e.errno, e.strerror))
+        sys.exit(1)
+    # Write pid.
+    pid=''
+    try:
+        f = file(pidfile, 'w')
+        pid = os.getpid()
+        f.write('%d' % pid)
+        f.close()
+    except IOError:
+        print >>sys.stderr, 'file=%s - daemonize: failed to write pid to %s' % (pidfile , pid)
+        logger.error('file=%s - daemonize: failed to write pid to %s' % (pidfile , pid))
+        sys.exit(1)
+
+    for f in sys.stdout, sys.stderr: f.flush()
+    sin = file(stdin, 'r')
+    sout = file(stdout, 'a+')
+    serr = file(stderr, 'a+')
+    os.dup2(sin.fileno(), sys.stdin.fileno())
+    os.dup2(sout.fileno(), sys.stdout.fileno())
+    os.dup2(serr.fileno(), sys.stderr.fileno())
+
+    return pid
+
+if __name__ == '__main__':
+    pass
diff --git a/pysilhouette/db/__init__.py b/pysilhouette/db/__init__.py
new file mode 100644 (file)
index 0000000..19c65e3
--- /dev/null
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import logging
+
+from sqlalchemy import create_engine, MetaData
+from sqlalchemy.orm import sessionmaker, mapper, \
+     clear_mappers, relation, scoped_session
+from sqlalchemy.orm.exc import UnmappedInstanceError
+
+from pysilhouette.db.model import reload_mappers
+from pysilhouette import SilhouetteException
+
+class SilhouetteDBException(SilhouetteException):
+    """Database running error.
+    """
+    pass
+
+class Database:
+    """TODO
+    """
+    
+    __engine = None
+    __metadata = None
+    __Session = None
+
+    def __init__(self, *args, **kwargs):
+        self.get_engine(*args, **kwargs)
+        self.create_metadata(self.__engine)
+
+    def get_engine(self, *args, **kwargs):
+        if not self.__engine:
+            self.__engine = create_engine(*args, **kwargs)
+        return self.__engine
+
+    def create_metadata(self, bind=None, reflect=False):
+        return MetaData(bind,reflect)
+
+    def get_metadata(self):
+        if not self.__metadata:
+            self.__metadata = self.create_metadata(self.__engine)
+        return self.__metadata
+
+    def get_session(self):
+        if self.__Session is None:
+            self.__Session = sessionmaker(bind=self.__engine)
+        return self.__Session()
+
+
+def dbsave(func):
+    """TODO
+    """
+
+    def wrapper(*args, **kwargs):
+        logger = logging.getLogger('pysilhouette.db')
+        session = args[0]
+        model = args[1]
+        model_name = repr(model).split("<")[0]
+        model_id = model.id
+        try:
+            func(*args, **kwargs)
+        except UnmappedInstanceError, ui:
+            logger.error(('Data to insert is failed, '
+                          'Invalid value was inputed. '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+            raise SilhouetteDBException(('Data to insert is failed, '
+                          'Invalid value was inputed. '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+
+        num = len(session.new)
+        if not num:
+            logger.warn('Data has not been changed. - %s=%s' %  (model_name, model_id))
+            return num  # The retrun value assume zero
+        
+        logger.debug('Data to insert is succeeded. - %s=%s' % (model_name, model_id))
+        return num
+    
+    wrapper.__name__ = func.__name__
+    wrapper.__dict__ = func.__dict__
+    wrapper.__doc__ = func.__doc__
+    return wrapper
+
+def dbupdate(func):
+    """TODO
+    """
+
+    def wrapper(*args, **kwargs):
+        logger = logging.getLogger('pysilhouette.db')
+        session = args[0]
+        model = args[1]
+        model_name = repr(model).split("<")[0]
+        model_id = model.id
+        try:
+            func(*args, **kwargs)
+        except UnmappedInstanceError, ui:
+            logger.error(('Data to update is failed, '
+                          'Invalid value was inputed '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+            raise SilhouetteDBException(('Data to update is failed, '
+                          'Invalid value was inputed. '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+        
+        num = len(session.dirty)
+        if not num:
+            logger.warn('Data has not been changed. - %s=%s' %  (model_name, model_id))
+            return num  # The retrun value assume zero
+        
+        logger.debug('Data to update is succeeded. - %s=%s' % (model_name, model_id))
+        return num
+    
+    wrapper.__name__ = func.__name__
+    wrapper.__dict__ = func.__dict__
+    wrapper.__doc__ = func.__doc__
+    return wrapper
+
+def dbdelete(func):
+    """TODO
+    """
+
+    def wrapper(*args, **kwargs):
+        logger = logging.getLogger('pysilhouette.db')
+        session = args[0]
+        model = args[1]
+        model_name = repr(model).split("<")[0]
+        model_id = model.id
+        try:
+            func(*args, **kwargs)
+        except UnmappedInstanceError, ui:
+            logger.error(('Data to delete is failed, '
+                          'Invalid value was inputed '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+            raise SilhouetteDBException(('Data to delete is failed, '
+                          'Invalid value was inputed. '
+                          '- %s=%s, error=%s') % (model_name, model_id, ''.join(ui)))
+
+        num = len(session.deleted)
+        if not num:
+            logger.warn('Data has not been changed. - %s=%s' %  (model_name, model_id))
+            return num  # The retrun value assume zero
+        
+        logger.debug('Data to delete is succeeded. - %s=%s' % (model_name, model_id))
+        return num
+    
+    wrapper.__name__ = func.__name__
+    wrapper.__dict__ = func.__dict__
+    wrapper.__doc__ = func.__doc__
+    return wrapper
+
+
+if __name__ == '__main__':
+    pass
diff --git a/pysilhouette/db/access.py b/pysilhouette/db/access.py
new file mode 100644 (file)
index 0000000..51642cf
--- /dev/null
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sqlalchemy
+import sqlalchemy.orm
+from pysilhouette.db import dbsave, dbupdate, dbdelete
+from  pysilhouette.db.model import JobGroup, Job, JOBGROUP_STATUS
+
+# JobGroup Table
+def jobgroup_findbyall(session, desc=False):
+    if desc is True:
+        return session.query(JobGroup).order_by(JobGroup.id.desc()).all()
+    else:
+        return session.query(JobGroup).order_by(JobGroup.id.asc()).all()
+
+def jobgroup_findbyall_limit(session, limit, desc=False):
+    if desc is True:
+        return session.query(JobGroup).order_by(JobGroup.modified.desc()).all()[:limit]
+    else:
+        return session.query(JobGroup).order_by(JobGroup.modified.asc()).all()[:limit]
+
+def jobgroup_findbystatus(session, status=JOBGROUP_STATUS['PEND']):
+    return session.query(JobGroup).filter(
+        JobGroup.status == status).order_by(JobGroup.id.asc()).all()
+
+def jobgroup_findbyuniqkey(session, uniq_key):
+    if uniq_key:
+        return session.query(JobGroup).filter(
+            JobGroup.uniq_key == uniq_key).all()
+    else:
+        return None
+
+def jobgroup_findbyid(session, jgid, uniq_key):
+    try:
+        return session.query(JobGroup).filter(
+            JobGroup.id == jgid).filter(JobGroup.uniq_key == uniq_key).one()
+    except sqlalchemy.orm.exc.NoResultFound, nrf:
+        return None
+
+
+def jobgroup_update(session, m_jg, status, autocommit=True):
+    m_jg.status = status
+    ret = update(session, m_jg)
+    if autocommit is True:
+        session.commit()
+    return ret
+
+# Edit
+@dbsave
+def save(session, model):
+    return session.save(model)
+
+@dbupdate
+def update(session, model):
+    return session.update(model)
+
+@dbdelete
+def delete(session, model):
+    return session.delete(model)
+
+
+# Job Table
+def job_findbyjobgroup_id(session, jgid, desc=False):
+    _q = session.query(Job).filter(Job.jobgroup_id == jgid)
+    if desc:
+        _r = _q.order_by(Job.order.desc()).all()
+    else:
+        _r = _q.order_by(Job.order.asc()).all()
+    return _r
+
+def job_update(session, m_job, status=None, autocommit=True):
+    if not status is None:
+        m_job.status = status
+        
+    ret = update(session, m_job)
+    
+    if autocommit is True:
+        session.commit()
+    return ret
+
+def job_result_action(session, job, info, autocommit=True):
+    job.action_exit_code = info['r_code']
+    job.action_stdout = info['stdout']
+    job.action_stderr = info['stderr']
+
+    ret = job_update(session, job)
+
+    if autocommit is True:
+        session.commit()
+
+    return ret
+
+def job_result_rollback(session, job, info, autocommit=True):
+    job.rollback_exit_code = info['r_code']
+    job.rollback_stdout = info['stdout']
+    job.rollback_stderr = info['stderr']
+
+    ret = job_update(session, job)
+
+    if autocommit is True:
+        session.commit()
+
+    return ret
+
+# progress
+def get_progress(session, job_id):
+    job = session.query(Job).filter(Job.id == job_id).one()
+    return job.progress
+
+def up_progress(session, job_id, up):
+    job = session.query(Job).filter(Job.id == job_id).one()
+    job.progress += up
+    if 100 < job.progress:
+        job.progress = 100
+    
+    job_update(session, job)
+    
+if __name__ == '__main__':
+    pass
diff --git a/pysilhouette/db/model.py b/pysilhouette/db/model.py
new file mode 100644 (file)
index 0000000..f22a9c7
--- /dev/null
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sqlalchemy
+import sqlalchemy.exc
+from sqlalchemy.orm import mapper, relation, clear_mappers
+
+from pysilhouette.util import is_empty
+
+# Job Constant
+_RES_CREATING = u'100' #: Creating
+_RES_PENDING = u'101' #: Pending
+_RES_RUNNING = u'102' #: Running
+_RES_ROLLBACK = u'103' #: Rollback running
+_RES_NORMAL_END = u'200' #: Normal end
+_RES_ROLLBACK_SUCCESSFUL_COMPLETION = u'201' #: Rollback successful completion
+_RES_ABNORMAL_TERMINATION = u'500' #: Abnormal termination
+_RES_ROLLBACK_ABEND = u'501' #: Rollback abend
+_RES_APP_ERROR = u'502' #: Application error
+_RES_WHITELIST_ERROR = u'503' #: Whitelist error
+
+#: Action command status.
+ACTION_STATUS = {
+    'PEND' : _RES_PENDING,
+    'RUN' : _RES_RUNNING,
+    'OK' : _RES_NORMAL_END,
+    'NG' : _RES_ABNORMAL_TERMINATION,
+    'WHITELIST' : _RES_WHITELIST_ERROR,
+    }
+#: Rollback command status.
+ROLLBACK_STATUS = {
+    'RUN' : _RES_ROLLBACK,
+    'OK' : _RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+    'NG' : _RES_ROLLBACK_ABEND,
+    'WHITELIST' : _RES_WHITELIST_ERROR,
+    }
+
+#: Jobgroup status
+JOBGROUP_STATUS = {
+    'PEND' : _RES_PENDING,
+    'RUN' : _RES_RUNNING,
+    'OK' : _RES_NORMAL_END,
+    'NG' : _RES_ABNORMAL_TERMINATION,
+    'APPERR' : _RES_APP_ERROR,
+    }
+
+#: Jobgroup Table instance.
+def get_jobgroup_table(metadata=sqlalchemy.MetaData()):
+    return sqlalchemy.Table('jobgroup', metadata,
+                            sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True,
+                                              autoincrement=True),
+                            sqlalchemy.Column('name', sqlalchemy.String(32), nullable=False),
+                            sqlalchemy.Column('uniq_key', sqlalchemy.Unicode(36), nullable=False),
+                            sqlalchemy.Column('finish_command', sqlalchemy.String(1024)), 
+                            sqlalchemy.Column('status', sqlalchemy.Unicode(3), nullable=False,
+                                              default=JOBGROUP_STATUS['PEND']),
+                            sqlalchemy.Column('register', sqlalchemy.String(32), nullable=True),
+                            sqlalchemy.Column('created', sqlalchemy.DateTime,
+                                              default=sqlalchemy.func.now()),
+                            sqlalchemy.Column('modified', sqlalchemy.DateTime,
+                                              default=sqlalchemy.func.now(),
+                                              onupdate=sqlalchemy.func.current_timestamp()),
+                            )
+
+#: Job Table instance.
+def get_job_table(metadata=sqlalchemy.MetaData()):
+    return sqlalchemy.Table('job', metadata,
+                            sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True,
+                                              autoincrement=True),
+                            sqlalchemy.Column('jobgroup_id', sqlalchemy.Integer, 
+                                              sqlalchemy.ForeignKey('jobgroup.id'),
+                                              index=True, nullable=False),
+                            sqlalchemy.Column('name', sqlalchemy.String(32), nullable=False),
+                            sqlalchemy.Column('order', sqlalchemy.Integer, nullable=False),
+                            sqlalchemy.Column('action_command', sqlalchemy.String(1024), nullable=False),
+                            sqlalchemy.Column('rollback_command', sqlalchemy.String(1024)),
+                            sqlalchemy.Column('status', sqlalchemy.Unicode(3), nullable=False,
+                                              default=ACTION_STATUS['PEND']),
+                            sqlalchemy.Column('action_exit_code', sqlalchemy.Integer),
+                            sqlalchemy.Column('action_stdout', sqlalchemy.TEXT),
+                            sqlalchemy.Column('action_stderr', sqlalchemy.TEXT),
+                            sqlalchemy.Column('rollback_exit_code', sqlalchemy.Integer),
+                            sqlalchemy.Column('rollback_stdout', sqlalchemy.TEXT),
+                            sqlalchemy.Column('rollback_stderr', sqlalchemy.TEXT),
+                            sqlalchemy.Column('progress', sqlalchemy.Integer, nullable=False, 
+                                              default=0),
+                            sqlalchemy.Column('created', sqlalchemy.DateTime, 
+                                              default=sqlalchemy.func.now()),
+                            sqlalchemy.Column('modified', sqlalchemy.DateTime,
+                                              default=sqlalchemy.func.now(),
+                                              onupdate=sqlalchemy.func.current_timestamp()),
+                            )
+
+def reload_mappers(metadata):
+    """all model mapper reload.
+    @param metadata: reload MetaData
+    @type metadata: sqlalchemy.schema.MetaData
+    """
+    t_jobgroup = get_jobgroup_table(metadata)
+    t_job = get_job_table(metadata)
+    try:
+        mapper(JobGroup, t_jobgroup, properties={'jobs': relation(Job)})
+        #mapper(JobGroup, t_jobgroup, properties={'jobs': relation(Job, backref='job_group')})
+        mapper(Job, t_job)
+    except sqlalchemy.exc.ArgumentError, ae:
+        clear_mappers()
+        mapper(JobGroup, t_jobgroup, properties={'jobs': relation(Job)})
+        #mapper(JobGroup, t_jobgroup, properties={'jobs': relation(Job, backref='job_group')})
+        mapper(Job, t_job)        
+    
+class Model(object):
+    """Model base class of all.
+    """
+    def utf8(self, column):
+        if hasattr(self, column):
+            ret = getattr(self, column)
+            if isinstance(ret, unicode):
+                return ret.encode('utf-8')
+            elif isinstance(ret, str):
+                return ret
+            else:
+                return str(ret)
+        else:
+            return 'not found.' # TODO: raise
+        
+class JobGroup(Model):
+    """JobGroup Table class.
+    """
+
+    def __init__(self, name, uniq_key):
+        self.name = name
+        self.uniq_key = uniq_key
+
+    def __repr__(self):
+        return "JobGroup<'%s','%s'>" % (self.name, self.uniq_key)
+        
+class Job(Model):
+    """Job Table class.
+    """
+    
+    #: Maximum number of characters to stdout.
+    STD_OUTPUT_LIMIT = 4096
+
+    def __init__(self, name, order, action_command):
+        self.name = name
+        self.order = order
+        self.action_command = action_command
+
+    def __repr__(self):
+        return "Job<'%s','%s','%s'>" % \
+               (self.name, self.order, self.action_command)
+
+    def is_rollback(self):
+        return not is_empty(self.rollback_command)
+
+if __name__ == '__main__':
+    """Testing
+    """
+    import sqlalchemy.orm
+    bind_name = 'sqlite:///:memory:'
+
+    engine = sqlalchemy.create_engine(bind_name, encoding="utf8", convert_unicode=True)
+    engine.echo = True
+    metadata = sqlalchemy.MetaData(bind=engine)
+    
+    t_jg = get_jobgroup_table(metadata)
+    t_job = get_job_table(metadata)
+    
+    sqlalchemy.orm.mapper(JobGroup, t_jg, 
+                          properties={'jobs': sqlalchemy.orm.relation(Job)})
+    sqlalchemy.orm.mapper(Job, t_job)
+    
+    metadata.drop_all()
+    metadata.create_all()
+    
+    Session = sqlalchemy.orm.sessionmaker(bind=engine)
+    session = Session()
+
+    # INSERT
+    jg = JobGroup(u'All Update', '192.168.0.100')
+    jg.jobs.append(Job(
+        u'Yum Update MySQL',
+        '0',
+        '/usr/bin/yum update mysql'))
+    jg.jobs.append(Job(
+        u'Yum Update PostgreSQL',
+        '1',
+        '/usr/bin/yum update postgresql'))
+    jg.jobs.append(Job(
+        u'Yum Update httpd',
+        '2',
+        '/usr/bin/yum update httpd'))
+    session.save(jg)
+    jg1 = JobGroup(u'get date', '172.16.0.123')
+    jg1.jobs.append(Job(u'get date','0', '/bin/date'))
+    jg2 = JobGroup(u'get route', '172.16.0.123')
+    jg2.jobs.append(Job(u'get route','0', '/sbin/route'))
+    jg3 = JobGroup(u'get ping', '172.16.0.123')
+    jg3.jobs.append(Job(u'get ping','0', '/bin/ping 172.16.0.1'))
+    session.add_all([jg1,jg2,jg3])
+    session.commit()
+
+    # SELECT One
+    jg = session.query(JobGroup).filter(JobGroup.name == u'All Update').one()
+    print jg.__repr__()
+    for jg in session.query(JobGroup).all():
+        print jg.__repr__()
+
+    # UPDATE
+    jg = session.query(JobGroup).filter(JobGroup.name == u'All Update').one()
+    jg.name = 'All Update - edit'
+    for j in jg.jobs:
+        j.name = 'All Update - edit'
+    session.update(jg)
+    session.commit()
+
+    # DELETE + Manual CASCADE
+    jgs = session.query(JobGroup).\
+                  filter(JobGroup.name.in_([u'get date', \
+                                           u'get route', \
+                                           u'get ping'])).all()
+    for jg in jgs:
+        for j in jg.jobs:
+            session.delete(j)
+        session.delete(jg)
+    session.commit()
diff --git a/pysilhouette/log.py b/pysilhouette/log.py
new file mode 100644 (file)
index 0000000..ed836ba
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import logging
+import logging.config
+
+#: logging ready
+ready = False
+
+def reload_conf(log_conf='/etc/opt/pysilhouette/log.conf'):
+    """Re-logging configuration
+    @param log_conf: configuration file path
+    @type log_conf: str
+    @rtype: bool
+    @return: ready
+    """
+    global ready
+    try:
+        logging.config.fileConfig(log_conf)
+        ready = True
+    except:
+        ready = False
+    return ready
+    
+def is_ready():
+    return ready
+
+if __name__ == '__main__':
+    """Testing
+    """
+    reload_conf('/etc/opt/pysilhouette/log.conf')
+    if is_ready():
+        _logger = logging.getLogger('pysilhouette.log')
+        _logger.debug('test')
+    else:
+        print >>sys.stderr('Loading configuration files still do not log.')
diff --git a/pysilhouette/performer.py b/pysilhouette/performer.py
new file mode 100644 (file)
index 0000000..3addab4
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import signal
+import os
+import traceback
+import logging
+
+from pysilhouette.log import reload_conf
+from pysilhouette.prep import readconf, getopts, chkopts
+from pysilhouette.db import Database
+from pysilhouette.db.model import reload_mappers, JOBGROUP_STATUS
+from pysilhouette.db.access import jobgroup_findbystatus, jobgroup_update
+from pysilhouette.worker import SimpleWorker
+
+from pysilhouette.util import kill_proc, write_pidfile, create_fifo
+
+def sigterm_handler(signum, frame):
+    logger = logging.getLogger('pysilhouette.performer.signal')
+    logger.info('Stop the performerd with signal - pid=%s, signal=%s' % (os.getpid(), signum))
+
+def performer(opts, cf):
+    logger = logging.getLogger('pysilhouette.performer')
+
+    # Initialization
+    if os.access(cf["observer.mkfifo.path"], os.F_OK|os.R_OK|os.W_OK) is False:
+        try:
+            os.unlink(cf["observer.mkfifo.path"])
+            logger.info('Deleted filo file. - file=%s' % cf["observer.mkfifo.path"])
+        except:
+            pass # Not anything
+        
+        create_fifo(cf["observer.mkfifo.path"],
+                    cf["observer.mkfifo.user.name"],
+                    cf["observer.mkfifo.group.name"],
+                    cf["observer.mkfifo.perms"],
+                    )
+
+        logger.info('The fifo file was created. - file=%s' % cf["observer.mkfifo.path"])
+
+
+    if opts.daemon is True:
+        pid = os.getpid()
+        if write_pidfile(opts.pidfile, pid):
+            logger.info('The process file was created. - file=%s' % opts.pidfile)
+        else:
+            logger.error('Could not create process file. - file=%s' % opts.pidfile)
+            return 1
+
+    logger.info('performer : [started]')
+
+    try:
+        db = Database(cf['database.url'],
+                      encoding="utf-8",
+                      convert_unicode=True,
+                      assert_unicode=False, # product
+                      #assert_unicode='warn', # dev
+                      #echo = opts.verbose,
+                      #echo_pool = opts.verbose,
+                      echo=True,
+                      echo_pool=True
+                      )
+
+        reload_mappers(db.get_metadata())
+
+    except Exception, e:
+        logger.error('Initializing a database error - %s' % ''.join(e.args))
+        t_logger = logging.getLogger('pysilhouette_traceback')
+        t_logger.error(traceback.format_exc())
+        return 1
+
+    while True:
+        fp = open(cf["observer.mkfifo.path"], 'r')
+        try:
+            code = fp.read()
+        finally:
+            fp.close()
+                
+        logger.info('Received code from the FIFO file. - code=%s' % code)
+        session = db.get_session()
+        m_jgs = jobgroup_findbystatus(session)
+        session.close()
+        
+        logger.info('Queued the Job Group from the database. - Number of JobGroup=%d' % len(m_jgs))
+        
+        if code == cf["observer.mkfifo.start.code"]:
+            if 0 < len(m_jgs):
+                for m_jg in m_jgs:
+                    try:
+                        w = SimpleWorker(cf, db, m_jg.id)
+                        w.run()
+                    except Exception, e:
+                        logger.info('Failed to perform the job group. Exceptions are not expected. - jobgroup_id=%d : %s'
+                                     % (m_jg.id, ','.join(e.args)))
+                        print >>sys.stderr, traceback.format_exc()
+                        t_logger = logging.getLogger('pysilhouette_traceback')
+                        t_logger.error(traceback.format_exc())
+
+                        try:
+                            session = db.get_session()
+                            jobgroup_update(session, m_jg, JOBGROUP_STATUS['APPERR'])
+                            session.close()
+                        except:
+                            logger.error('Failed to change the status of the job group. - jobgroup_id=%d : %s'
+                                         % (m_jg.id, ','.join(e.args)))
+                            t_logger = logging.getLogger('pysilhouette_traceback')
+                            t_logger.error(traceback.format_exc())
+                            
+            else:
+                logger.info('No Job Group.')
+        elif code == cf["observer.mkfifo.stop.code"]:
+            logger.info('Received stop code from the FIFO file. - code=%s' % code)
+            break
+        else:
+            logger.info('Received illegal code from the FIFO file. - code=%s' % code)
+
+def main():
+    (opts, args) = getopts()
+    if chkopts(opts) is True:
+        return 1
+    
+    cf = readconf(opts.config)
+    if cf is None:
+        print >>sys.stderr, 'Failed to load the config file "%s". (%s)' % (opts.config, sys.argv[0])
+        return 1
+
+    # set env=PYSILHOUETTE_CONF
+    os.environ['PYSILHOUETTE_CONF'] = opts.config
+    
+    if reload_conf(cf["env.sys.log.conf.path"]):
+        logger = logging.getLogger('pysilhouette.performer')
+    else:
+        print >>sys.stderr, 'Failed to load the log file. (%s)' % sys.argv[0]
+        return 1
+
+    try:
+        try:
+            signal.signal(signal.SIGTERM, sigterm_handler)
+            ret = performer(opts, cf) # start!!
+            return ret
+        except KeyboardInterrupt, k:
+            logger.critical('Keyboard interrupt occurred. - %s' % ''.join(k.args))
+            print >>sys.stderr, 'Keyboard interrupt occurred. - %s' % ''.join(k.args)
+        except Exception, e:
+            logger.critical('System error has occurred. - %s' % ''.join(e.args))
+            print >>sys.stderr, 'System error has occurred. - %s' % ''.join(e.args)
+            print >>sys.stderr, traceback.format_exc()
+            t_logger = logging.getLogger('pysilhouette_traceback')
+            t_logger.critical(traceback.format_exc())
+            
+    finally:
+        if opts.daemon is True and os.path.isfile(opts.pidfile):
+            os.unlink(opts.pidfile)
+            logger.info('Process file has been deleted.. - pidfile=%s' % opts.pidfile)
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/pysilhouette/prep.py b/pysilhouette/prep.py
new file mode 100644 (file)
index 0000000..9e352f0
--- /dev/null
@@ -0,0 +1,279 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import re
+import sys
+import os
+import pwd
+import grp
+from optparse import OptionParser
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+from pysilhouette import __version__
+
+usage = '%prog [options]'
+
+def getopts():
+    optp = OptionParser(usage=usage, version=__version__)
+    optp.add_option('-c', '--config', dest='config', help='configuration file')
+    optp.add_option('-d', '--daemon', dest='daemon', action="store_true", help='Daemon startup')
+    optp.add_option('-v', '--verbose', dest='verbose', action="store_true", help='Has not been used.')
+    optp.add_option('-p', '--pidfile', dest='pidfile', action="store", type='string', help='process file path')
+    optp.add_option('-k', '--uniqkey', dest='uniqkey', action="store_true", help='show unique key')
+    return optp.parse_args()
+
+def chkopts(opts):
+    if not opts.config:
+        print >>sys.stderr, '-c or --config option is required.'
+        return True
+
+    if os.path.isfile(opts.config) is False:
+        print >>sys.stderr, '-c or --config file is specified in the option does not exist.'
+        return True
+
+    if opts.uniqkey:
+        return False
+
+    if opts.daemon is True and not opts.pidfile:
+        print >>sys.stderr, '-p or --pidfile option is required.'
+        return True
+
+    if not opts.daemon and opts.pidfile:
+        print >>sys.stderr, '-p doesn work without -d. Please add the option to -d or --daemon.'
+        return True
+
+    return False
+
+def chk_conf(cf):
+    def is_key(key):
+        if cf.has_key(key) is True and 0 < len(cf[key]):
+            return True
+        else:
+            return False
+
+    # env
+    err_key = ""
+    if len(err_key) <= 0 and is_key("env.python") is False:
+        err_key = "env.python"
+    if len(err_key) <= 0 and is_key("env.sys.log.conf.path") is False:
+        err_key = "env.sys.log.conf.path"
+    if len(err_key) <= 0 and is_key("env.uniqkey") is False:
+        err_key = "env.uniqkey"
+    if len(err_key) <= 0 and is_key("daemon.stdin") is False:
+        err_key = "daemon.stdin"
+    if len(err_key) <= 0 and is_key("daemon.stdout") is False:
+        err_key = "daemon.stdout"
+    if len(err_key) <= 0 and is_key("daemon.stderr") is False:
+        err_key = "daemon.stderr"
+    if len(err_key) <= 0 and is_key("observer.target.python") is False:
+        err_key = "observer.target.python"
+    if len(err_key) <= 0 and is_key("observer.target.scheduler") is False:
+        err_key = "observer.target.scheduler"
+    if len(err_key) <= 0 and is_key("observer.target.performer") is False:
+        err_key = "observer.target.performer"
+    if len(err_key) <= 0 and is_key("observer.restart.count") is False:
+        err_key = "observer.restart.count"
+    if len(err_key) <= 0 and is_key("observer.restart.count.clear.time") is False:
+        err_key = "observer.restart.count.clear.time"
+    if len(err_key) <= 0 and is_key("observer.check.interval") is False:
+        err_key = "observer.check.interval"
+    if len(err_key) <= 0 and is_key("observer.status.path") is False:
+        err_key = "observer.status.path"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.path") is False:
+        err_key = "observer.mkfifo.path"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.start.code") is False:
+        err_key = "observer.mkfifo.start.code"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.ignore.code") is False:
+        err_key = "observer.mkfifo.ignore.code"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.stop.code") is False:
+        err_key = "observer.mkfifo.stop.code"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.user.name") is False:
+        err_key = "observer.mkfifo.user.name"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.group.name") is False:
+        err_key = "observer.mkfifo.group.name"
+    if len(err_key) <= 0 and is_key("observer.mkfifo.perms") is False:
+        err_key = "observer.mkfifo.perms"
+    if len(err_key) <= 0 and is_key("scheduler.interval") is False:
+        err_key = "scheduler.interval"
+    if len(err_key) <= 0 and is_key("job.popen.env.lang") is False:
+        err_key = "job.popen.env.lang"
+    if len(err_key) <= 0 and is_key("job.popen.timeout") is False:
+        err_key = "job.popen.timeout"
+    if len(err_key) <= 0 and is_key("job.popen.waittime") is False:
+        err_key = "job.popen.waittime"
+    if len(err_key) <= 0 and is_key("database.url") is False:
+        err_key = "database.url"
+
+    if 0 < len(err_key):
+        print >>sys.stderr, 'Configuration files are missing. - %s' % (err_key)
+        return False
+
+    if os.access(cf["env.python"], os.R_OK | os.X_OK) is False:
+        print >>sys.stderr, 'Incorrect file permissions. - env.python=%s' % (cf["env.python"])
+        return False
+
+    if os.access(cf["observer.target.python"], os.R_OK | os.X_OK) is False:
+        print >>sys.stderr, 'Incorrect file permissions. - observer.target.python=%s' % (cf["observer.target.python"])
+        return False
+
+    if os.access(cf["observer.target.scheduler"], os.R_OK) is False:
+        print >>sys.stderr, 'Incorrect file permissions. - observer.target.scheduler=%s' % (cf["observer.target.scheduler"])
+        return False
+
+    if os.access(cf["observer.target.performer"], os.R_OK) is False:
+        print >>sys.stderr, 'Incorrect file permissions. - observer.target.performer=%s' % (cf["observer.target.performer"])
+        return False
+
+    from pysilhouette.util import is_int
+    if is_int(cf["observer.restart.count"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.restart.count=%s' % (cf["observer.restart.count"])
+        return False
+
+    if is_int(cf["observer.restart.count.clear.time"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.restart.count.clear.time=%s' % (cf["observer.restart.count.clear.time"])
+        return False
+
+    if is_int(cf["observer.check.interval"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.check.interval=%s' % (cf["observer.check.interval"])
+        return False
+
+    if is_int(cf["observer.mkfifo.start.code"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.mkfifo.start.code=%s' % (cf["observer.mkfifo.start.code"])
+        return False
+
+    if is_int(cf["observer.mkfifo.ignore.code"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.mkfifo.ignore.code=%s' % (cf["observer.mkfifo.ignore.code"])
+        return False
+
+    if is_int(cf["observer.mkfifo.stop.code"]) is False:
+        print >>sys.stderr, 'Must be a number. - observer.mkfifo.stop.code=%s' % (cf["observer.mkfifo.stop.code"])
+        return False
+
+    if is_int(cf["scheduler.interval"]) is False:
+        print >>sys.stderr, 'Must be a number. - scheduler.interval=%s' % (cf["scheduler.interval"])
+        return False
+
+    if is_int(cf["job.popen.timeout"]) is False:
+        print >>sys.stderr, 'Must be a number. - job.popen.timeout=%s' % (cf["job.popen.timeout"])
+        return False
+
+    if is_int(cf["job.popen.waittime"]) is False:
+        print >>sys.stderr, 'Must be a number. - job.popen.waittime=%s' % (cf["job.popen.waittime"])
+        return False
+
+    s_mkfifo = set([cf["observer.mkfifo.start.code"],
+                    cf["observer.mkfifo.ignore.code"],
+                    cf["observer.mkfifo.stop.code"]],
+                   )
+    if len(s_mkfifo) != 3:
+        print >>sys.stderr, 'Is not unique. - observer.mkfifo.[start,ignore,stop]=%s,%s,%s' \
+              % (cf["observer.mkfifo.start.code"],
+                 cf["observer.mkfifo.ignore.code"],
+                 cf["observer.mkfifo.stop.code"],
+                 )
+        return False
+
+    try:
+        pwd.getpwnam(cf["observer.mkfifo.user.name"])
+    except:
+        print >>sys.stderr, 'Can not get information of the user (nonexistent?). - observer.mkfifo.user.name=%s' % (cf["observer.mkfifo.user.name"])
+
+    try:
+        grp.getgrnam(cf["observer.mkfifo.group.name"])
+    except:
+        print >>sys.stderr, 'Can not get information of the group (nonexistent?). - observer.mkfifo.group.name=%s' % (cf["observer.mkfifo.group.name"])
+
+    try:
+        int(cf["observer.mkfifo.perms"], 8)
+    except:
+        print >>sys.stderr, 'Incorrect file permissions. - observer.mkfifo.perms=%s' % (cf["observer.mkfifo.perms"])
+        return False
+
+    if cf.has_key("job.whitelist.flag") is True \
+           and cf["job.whitelist.flag"] == "1" \
+           and cf.has_key("job.whitelist.path") is True \
+           and 0 < len(cf["job.whitelist.path"]):
+        if os.path.isfile(cf["job.whitelist.path"]) is False:
+            print >>sys.stderr, 'File not found. - job.whitelist.path=%s' % (cf["job.whitelist.path"])
+            return False
+
+    return True
+
+def readconf(path):
+    if not os.path.isfile(path):
+        print >>sys.stderr, 'file=%s - file specified in the option does not exist.' % path
+        return None
+
+    fp = open(path, 'r')
+    try:
+        try:
+            _r = {}
+            for line in fp:
+                line = re.sub(r'[ \t]', '', line).strip()
+                if len(line) <= 0 or line[0] == '#':
+                    continue
+                key, value = line.split('=', 1)
+                try:
+                    value = value[:value.rindex('#')]
+                except ValueError,ve:
+                    pass
+                _r[key] = value
+            return _r
+        except Exception, e:
+            print >>sys.stderr, 'file=%s - Failed to load configuration files. : except=%s' \
+                  % (path, e.args)
+            return None
+    finally:
+        fp.close()
+
+def sysappend(pkg):
+    lines = []
+    if type(pkg) is list:
+        lines = pkg
+    else:
+        print >>sys.stderr, '%s should be list type.' % (pkg,)
+        return False
+        
+    for line in lines:
+        f = False
+        for path in sys.path:
+            if line == path:
+                f = True
+                break
+        if f is False:
+            sys.path.insert(1, line)
+    return True
+
+if __name__ == "__main__":
+    pass
diff --git a/pysilhouette/scheduler.py b/pysilhouette/scheduler.py
new file mode 100644 (file)
index 0000000..3bf2b7f
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import os
+import sys
+import time
+import signal
+import traceback
+import logging
+
+from pysilhouette.log import reload_conf
+from pysilhouette.prep import readconf, getopts, chkopts
+from pysilhouette.util import write_pidfile, create_fifo
+
+def sigterm_handler(signum, frame):
+    global logger
+    logger.info('Stop the schedulerd with signal- pid=%s, signal=%s' % (os.getpid(), signum))
+
+def scheduler():
+    global opts
+    global cf
+    global logger
+
+    if os.access(cf["observer.mkfifo.path"], os.F_OK|os.R_OK|os.W_OK) is False:
+        try:
+            os.unlink(cf["observer.mkfifo.path"])
+            logger.info('Deleted filo file. - file=%s' % cf["observer.mkfifo.path"])
+        except:
+            pass # Not anything
+        create_fifo(cf["observer.mkfifo.path"],
+                    cf["observer.mkfifo.user.name"],
+                    cf["observer.mkfifo.group.name"],
+                    cf["observer.mkfifo.perms"],
+                    )
+
+        logger.info('The fifo file was created. - file=%s' % cf["observer.mkfifo.path"])
+
+    if opts.daemon is True:
+        pid = os.getpid()
+        if write_pidfile(opts.pidfile, pid):
+            logger.info('The process file was created. - file=%s' % opts.pidfile)
+        else:
+            logger.info('Could not create process file. - file=%s' % opts.pidfile)
+            return 1
+
+    logger.info('schedulerd started!!')
+    
+    while True:
+        try:
+            fp = open(cf["observer.mkfifo.path"], 'w')
+            try:
+                fp.write(cf['observer.mkfifo.start.code'])
+                logger.info('Start code was written. - file=%s : code=%s'
+                            % (cf["observer.mkfifo.path"], cf['observer.mkfifo.start.code']))
+            finally:
+                fp.close()
+
+            logger.debug('interval start, interval=%s' % (cf['scheduler.interval']))
+            time.sleep(int(cf['scheduler.interval']))
+        except IOError, i:
+            if i.errno == 4:
+                return 0 # When ending with the signal
+
+    # beyond expectation
+    logger.error('file=%s - 2 error write FIFO, code=%s'
+                     % (self.fifo, cf['observer.mkfifo.start.code']))
+    return 1
+    
+def main(): 
+    global opts
+    global cf
+    global logger
+
+    (opts, args) = getopts()
+    if chkopts(opts) is True:
+        return 1
+
+    cf = readconf(opts.config)
+    if cf is None:
+        print >>sys.stderr, 'Failed to load the config file "%s". (%s)' % (opts.config, sys.argv[0])
+        return 1
+    
+    if reload_conf(cf["env.sys.log.conf.path"]):
+        logger = logging.getLogger('pysilhouette.scheduler')
+    else:
+        print >>sys.stderr, 'Failed to load the log file. (%s)' % sys.argv[0]
+        return 1
+    
+    try:
+        try:
+            signal.signal(signal.SIGTERM, sigterm_handler)
+            ret = scheduler() # start!!
+            return ret
+        except KeyboardInterrupt, k:
+            logger.critical('Keyboard interrupt occurred. - %s' % ''.join(k.args))
+            print >>sys.stderr, 'Keyboard interrupt occurred. - %s' % ''.join(k.args)
+        except Exception, e:
+            logger.critical('A system error has occurred. - %s' % ''.join(e.args))
+            print >>sys.stderr, 'A system error has occurred. - %s' % ''.join(e.args)
+            print >>sys.stderr, traceback.format_exc()
+            t_logger = logging.getLogger('pysilhouette_traceback')
+            t_logger.critical(traceback.format_exc())
+            
+    finally:
+        if opts.daemon is True and os.path.isfile(opts.pidfile):
+            os.unlink(opts.pidfile)
+            logger.info('Process file has been deleted.. - pidfile=%s' % opts.pidfile)
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/pysilhouette/silhouette.py b/pysilhouette/silhouette.py
new file mode 100644 (file)
index 0000000..b7761c6
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import os
+import sys
+import signal
+import logging
+import traceback
+
+try:
+    import sqlalchemy
+except ImportError, e:
+    print >>sys.stderr, '[Error] There are not enough libraries. - %s' % ''.join(e.args)
+    #traceback.format_exc()
+    sys.exit(1)
+    
+from pysilhouette.prep import readconf, getopts, chkopts, chk_conf
+from pysilhouette.daemon import daemonize, observer
+from pysilhouette.log import reload_conf
+
+opt = None #: command options
+
+def sigterm_handler(signum, frame):
+    global opt
+    logger = logging.getLogger('pysilhouette.silhouette.signal')
+    logger.info('Stop the schedulerd with signal- pid=%s, signal=%s' % (os.getpid(), signum))
+    if opts.daemon is True and os.path.isfile(opts.pidfile):
+        os.unlink(opts.pidfile)
+        logger.info('Process file has been deleted.. - pidfile=%s' % opts.pidfile)
+
+def main():
+    global opts
+
+    (opts, args) = getopts()
+    if chkopts(opts) is True:
+        return 1
+    
+    ####
+    try:
+        opts.config = os.path.abspath(opts.config)
+    except AttributeError, e:
+        print >>sys.stderr, 'No configuration file path.'
+        return 1
+    
+    cf = readconf(opts.config)
+    if cf is None:
+        print >>sys.stderr, 'Failed to load the config file "%s". (%s)' % (opts.config, sys.argv[0])
+        return 1
+
+    # conf check
+    if chk_conf(cf) is False:
+        return 1
+    
+    if reload_conf(cf["env.sys.log.conf.path"]):
+        logger = logging.getLogger('pysilhouette.silhouette')
+    else:
+        print >>sys.stderr, 'Failed to load the log file. (%s)' % sys.argv[0]
+        return 1
+
+    if opts.uniqkey:
+        print >>sys.stdout, cf["env.uniqkey"]
+        return 0
+
+    if opts.daemon is True:
+        logger.debug('Daemon stdin=%s' % cf['daemon.stdin'])
+        logger.debug('Daemon stdout=%s' % cf['daemon.stdout'])
+        logger.debug('Daemon stderr=%s' % cf['daemon.stderr'])
+        pid = daemonize(stdin=cf['daemon.stdin'],
+                        stdout=cf['daemon.stdout'],
+                        stderr=cf['daemon.stderr'],
+                        pidfile=opts.pidfile)
+        logger.info('Daemon Running!! pid=%s' % pid)
+        
+    try:
+        signal.signal(signal.SIGTERM, sigterm_handler)
+        ret = observer(opts=opts, cf=cf) # start!!
+        return ret
+    except KeyboardInterrupt, k:
+        logger.critical('Keyboard interrupt occurred. - %s' % ''.join(k.args))
+        print >>sys.stderr, 'Keyboard interrupt occurred. - %s' % ''.join(k.args)
+    except Exception, e:
+        logger.critical('System error has occurred. - %s' % ''.join(e.args))
+        print >>sys.stderr, 'System error has occurred. - %s' % ''.join(e.args)
+        t_logger = logging.getLogger('pysilhouette_traceback')
+        t_logger.critical(traceback.format_exc())
+        print >>sys.stderr, traceback.format_exc()
+            
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/pysilhouette/tests/__init__.py b/pysilhouette/tests/__init__.py
new file mode 100644 (file)
index 0000000..4fd2368
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
diff --git a/pysilhouette/tests/suite.py b/pysilhouette/tests/suite.py
new file mode 100644 (file)
index 0000000..df7d4df
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import unittest
+
+from pysilhouette.tests.testprep import all_suite_prep
+from pysilhouette.tests.testworker import all_suite_worker
+
+ts = unittest.TestSuite()
+ts.addTest(all_suite_prep())
+ts.addTest(all_suite_worker())
+unittest.TextTestRunner(verbosity=2).run(ts)
diff --git a/pysilhouette/tests/testprep.py b/pysilhouette/tests/testprep.py
new file mode 100644 (file)
index 0000000..a31ebeb
--- /dev/null
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import os
+import unittest
+import pysilhouette.prep
+
+class TestPrep(unittest.TestCase):
+
+    _sysappend0 = ['hoge', 'foo']
+    _sysappend1 = []
+    _sysappend2 = ''
+    _sysappend3 = None
+
+    _tmpfile = None
+
+    def setUp(self):
+        # readconf
+        import tempfile
+        self._tmpfile = tempfile.mkstemp()
+        fp = open(self._tmpfile[1], 'w')
+        fp.write('key0=value0\n')
+        fp.write('key1 =value1\n')
+        fp.write('key2= value2\n')
+        fp.write('key3 = value3\n')
+        fp.write('#key4=value4\n')
+        fp.write('k#ey5=value5\n')
+        fp.close()
+        pass
+    
+    def tearDown(self):
+        os.unlink(self._tmpfile[1])
+        pass
+        
+    def test_sysappend_0(self):
+        ret = pysilhouette.prep.sysappend(self._sysappend0)
+        self.assertEqual(ret, True)
+        for target in self._sysappend0:
+            val = False
+            for line in sys.path:
+                if target == line:
+                    val = True
+            if val is False:
+                self.fail('sys.path insert error!!')
+
+    def test_sysappend_1(self):
+        ret = pysilhouette.prep.sysappend(self._sysappend1)
+        self.assertEqual(ret, True)
+
+    def test_sysappend_2(self):
+        ret = pysilhouette.prep.sysappend(self._sysappend2)
+        self.assertEqual(ret, False)
+
+    def test_sysappend_3(self):
+        ret = pysilhouette.prep.sysappend(self._sysappend3)
+        self.assertEqual(ret, False)
+
+    def test_readconf_0(self):
+        ret = pysilhouette.prep.readconf(self._tmpfile[1])
+        if ret['key0'] != 'value0':
+            self.fail()
+        if ret['key1'] != 'value1':
+            self.fail()
+        if ret['key2'] != 'value2':
+            self.fail()
+        if ret['key3'] != 'value3':
+            self.fail()
+        try:
+            if ret['#key4'] != 'value4':
+                self.fail()
+        except KeyError:
+            pass
+        if ret['k#ey5'] != 'value5':
+            self.fail()
+
+class SuiteSysappend(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_sysappend_0', 'test_sysappend_1',
+                 'test_sysappend_2', 'test_sysappend_3']
+        unittest.TestSuite.__init__(self,map(TestPrep, tests))
+
+class SuiteReadconf(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_readconf_0']
+        unittest.TestSuite.__init__(self,map(TestPrep, tests))
+
+def all_suite_prep():
+    return unittest.TestSuite([SuiteSysappend(), SuiteReadconf()])
+
+if __name__ == '__main__':
+    unittest.TextTestRunner(verbosity=2).run(all_suite_prep())
diff --git a/pysilhouette/tests/testutil.py b/pysilhouette/tests/testutil.py
new file mode 100644 (file)
index 0000000..c2468ee
--- /dev/null
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import os
+import unittest
+import pysilhouette.util as target
+
+class TestUtil(unittest.TestCase):
+
+    fname = '/tmp/testutil.fifo'
+    pname = '/tmp/testutil.pid'
+
+    def setUp(self):
+        pass
+    
+    def tearDown(self):
+        pass
+
+    def unlink(self, name):
+        try:
+            os.unlink(name)
+        except:
+            pass
+
+    def test_popen_0(self):
+        cmd = target.split_shell_command('date')
+        (proc, proc_info) = target.popen(cmd)
+        self.assertTrue(proc_info['r_code'] == 0)
+        
+    def test_popen_1(self):
+        cmd = target.split_shell_command('cat *')
+        (proc, proc_info) = target.popen(cmd)
+        self.assertTrue(proc_info['r_code'] == 1)
+
+    def test_split_shell_command_0(self):
+        ret = target.split_shell_command('date')
+        self.assertTrue(type(ret) is list)
+        self.assertTrue(len(ret) == 1)
+        self.assertTrue(ret[0] == 'date')
+
+        ret = target.split_shell_command('date -a')
+        self.assertTrue(type(ret) is list)
+        self.assertTrue(len(ret) == 2)
+        self.assertTrue((ret[0] == 'date' and ret[1] == '-a'))
+        
+
+        ret = target.split_shell_command(' date  -a ')
+        self.assertTrue(type(ret) is list)
+        self.assertTrue(len(ret) == 2)
+        self.assertTrue((ret[0] == 'date' and ret[1] == '-a'))
+
+        ret = target.split_shell_command('    date               -a        ')
+        self.assertTrue(type(ret) is list)
+        self.assertTrue(len(ret) == 2)
+        self.assertTrue((ret[0] == 'date' and ret[1] == '-a'))
+        
+    def test_split_shell_command_1(self):
+        self.assertFalse(target.split_shell_command(None))
+
+
+    def test_is_empty_0(self):
+        self.assertFalse(target.is_empty('cmd'))
+        self.assertFalse(target.is_empty(' cmd '))
+        self.assertFalse(target.is_empty(' cmd'))
+        self.assertFalse(target.is_empty('cmd -a '))
+        self.assertFalse(target.is_empty('cmd -a  '))
+        self.assertFalse(target.is_empty('cmd -a /hoge'))
+        self.assertFalse(target.is_empty('cmd -a -as'))
+        self.assertFalse(target.is_empty('cmd a -as'))
+        self.assertTrue(target.is_empty(''))
+        self.assertTrue(target.is_empty(' '))
+        self.assertTrue(target.is_empty(None))
+
+    def test_create_fifo_0(self):
+        self.unlink(self.fname)
+        ret = target.create_fifo(self.fname,'satori','pysilhouette','0641')
+        self.assertTrue(ret)
+        self.unlink(self.fname)
+
+    def test_create_fifo_1(self):
+        target.create_fifo(self.fname,'root','root','0641')
+        ret = target.create_fifo(self.fname,'root','root','0641')
+        self.assertFalse(ret)
+        self.unlink(self.fname)
+
+    def test_write_pidfile_0(self):
+        self.unlink(self.pname)
+        ret = target.write_pidfile(self.pname, 12345)
+        self.assertTrue(ret)
+        self.unlink(self.pname)
+
+    def test_write_pidfile_1(self):
+        self.unlink(self.pname)
+        ret = target.write_pidfile(self.pname, 10)
+        ret = target.write_pidfile(self.pname, 20)
+        fp = open(self.pname, 'r')
+        ret = fp.read()
+        self.assertEquals('20', ret)
+        self.unlink(self.pname)
+
+    def test_read_pidfile_0(self):
+        self.unlink(self.pname)
+        target.write_pidfile(self.pname, 30)
+
+        ret = target.read_pidfile(self.pname)
+        self.assertEquals('30', ret)
+        self.unlink(self.pname)
+
+    def test_read_pidfile_1(self):
+        self.unlink(self.pname)
+
+        ret = target.read_pidfile(self.pname)
+        self.assertEquals('', ret)
+
+class SuiteIsSplitShellCommand(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_split_shell_command_0',
+                 'test_split_shell_command_1',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+class SuiteIsEmpty(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_is_empty_0',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+class SuiteCreateFifo(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_create_fifo_0',
+                 'test_create_fifo_1',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+class SuitePopen(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_popen_0',
+                 'test_popen_1',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+class SuiteWritePidfile(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_write_pidfile_0',
+                 'test_write_pidfile_1',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+class SuiteReadPidfile(unittest.TestSuite):
+    def __init__(self):
+        tests = ['test_read_pidfile_0',
+                 'test_read_pidfile_1',
+                 ]
+        unittest.TestSuite.__init__(self,map(TestUtil, tests))
+
+def all_suite_util():
+    return unittest.TestSuite([SuiteIsSplitShellCommand(),
+                               SuitePopen(),
+                               SuiteIsEmpty(),
+                               SuiteCreateFifo(),
+                               SuiteWritePidfile(),
+                               SuiteReadPidfile(),
+                               ])
+
+if __name__ == '__main__':
+    unittest.TextTestRunner(verbosity=2).run(all_suite_util())
diff --git a/pysilhouette/tests/testworker.py b/pysilhouette/tests/testworker.py
new file mode 100644 (file)
index 0000000..de4b932
--- /dev/null
@@ -0,0 +1,886 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import os
+import unittest
+import logging
+
+import sqlalchemy.orm
+
+from pysilhouette.prep import readconf 
+from pysilhouette.db import Database
+from pysilhouette.db.model import RES_PENDING, RES_RUNNING,RES_NORMAL_END, \
+    RES_ABNORMAL_TERMINATION, RES_ROLLBACK,RES_ROLLBACK_ABEND, \
+    RES_ROLLBACK_SUCCESSFUL_COMPLETION, reload_mappers
+from pysilhouette.worker import Worker
+from pysilhouette.db.access import *
+
+class TestWorker(unittest.TestCase):
+    """
+    """
+    _db = None
+
+    def setUp(self):
+        # O/R mapping
+        self._db = Database(cf['database.url'], encoding="utf-8", convert_unicode=True, echo=True)
+        reload_mappers(self._db.get_metadata())
+
+        # database init
+        self._db.get_metadata().drop_all()
+        self._db.get_metadata().create_all() # create table
+        pass
+
+    def tearDown(self):
+        sqlalchemy.orm.clear_mappers()
+        pass
+
+    def set_job(self, session, jg_name, uniqkey, 
+                job=(True, True, True), rollback=None, mail=None):
+        jg1 = JobGroup(jg_name.decode('utf-8'), uniqkey)
+
+        if job[0] is True:
+            j1 = Job(u'file create','0','/bin/touch /tmp/test_case1.txt')
+        else:
+            j1 = Job(u'file create','0','/bin/touch_dummy /tmp/test_case1.txt')
+
+        if job[1] is True:
+            j2 = Job(u'file copy', '1', '/bin/cp /tmp/test_case1.txt /tmp/test_case1_rename.txt')
+        else:
+            j2 = Job(u'file copy', '1', '/bin/cp_dummy /tmp/test_case1.txt /tmp/test_case1_rename.txt')
+
+        if job[2] is True:
+            j3 = Job(u'file delete','2','/bin/rm /tmp/test_case1.txt')
+        else:
+            j3 = Job(u'file delete','2','/bin/rm_dummy /tmp/test_case1.txt')
+
+        jg1.jobs.append(j1)
+        jg1.jobs.append(j2)
+        jg1.jobs.append(j3)
+
+        if rollback is True:
+            j1.rollback_command = u'/bin/echo JOB1 rollback'
+            j2.rollback_command = u'/bin/echo JOB2 rollback'
+            j3.rollback_command = u'/bin/echo JOB3 rollback'
+        elif rollback is False:
+            j1.rollback_command = u'/bin/echo_dummy JOB1 rollback'
+            j2.rollback_command = u'/bin/echo_dummy JOB2 rollback'
+            j3.rollback_command = u'/bin/echo_dummy JOB3 rollback'
+        elif rollback is None:
+            pass
+
+        if mail is True:
+            jg1.finish_command = ok_f_cmd % (jg1.name, jg1.name)
+        elif mail is False:
+            jg1.finish_command = ng_f_cmd % (jg1.name, jg1.name)
+        elif mail is None:
+            pass
+
+        session.add_all([jg1])
+        session.commit()
+
+    def run_job(self, sess):
+        _m_jgs = jobgroup_findbystatus(sess)
+        for _m_jg in _m_jgs:
+            _w = Worker(self._db, _m_jg.id)
+            _w.run()
+            worker_debug(self._db, _m_jg.id)
+        sess.close()
+        
+    def check_job(self, sess, jg=RES_NORMAL_END, 
+                  job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END)):
+        sess = self._db.get_session()
+        _m_jg1 = jobgroup_findbyid(sess, 1)
+        self.assertEqual(int(_m_jg1.status), int(jg))
+
+        _m_jobs = job_findbyjobgroup_id(sess, 1, False)
+        self.assertEqual(int(_m_jobs[0].status), int(job[0]))
+        self.assertEqual(int(_m_jobs[1].status), int(job[1]))
+        self.assertEqual(int(_m_jobs[2].status), int(job[2]))
+
+        sess.close()
+        
+
+    def test_case1(self):
+        """Test Case 1
+          - Job Group : Normal end
+          - Job : All Normal end
+          - Rollback: Normal end
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 1', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, True), rollback=True, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_NORMAL_END, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END))
+
+
+    def test_case2(self):
+        """Test Case 2
+          - Job Group : Normal end
+          - Job : All Normal end
+          - Rollback: Abnormal termination
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 2', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, True), rollback=False, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_NORMAL_END, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END))
+
+        sess.close()
+
+
+    def test_case3(self):
+        """Test Case 3
+          - Job Group : Normal end
+          - Job : All Normal end
+          - Rollback: None
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 3', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, True), rollback=None, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_NORMAL_END, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END))
+
+        sess.close()
+
+    def test_case4(self):
+        """Test Case 4
+          - Job Group : Normal end
+          - Job : All Normal end
+          - Rollback: Normal end
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 4', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, True), rollback=None, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_NORMAL_END, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END))
+
+        sess.close()
+
+
+    def test_case5(self):
+        """Test Case 5
+          - Job Group : Normal end
+          - Job : All Normal end
+          - Rollback: None
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 5', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, True), rollback=None, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_NORMAL_END, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_NORMAL_END))
+
+        sess.close()
+
+    def test_case6(self):
+        """Test Case 6
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 6', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=True, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION, 
+                            RES_PENDING,
+                            RES_PENDING))
+        sess.close()
+
+    def test_case7(self):
+        """Test Case 7
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 7', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=False, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_ABEND, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+
+    def test_case8(self):
+        """Test Case 8
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: None
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 8', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=None, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+              job=(RES_ABNORMAL_TERMINATION, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+    def test_case9(self):
+        """Test Case 9
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 9', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=True, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_PENDING,
+                            RES_PENDING))
+
+        sess.close()
+
+    def test_case10(self):
+        """Test Case 10
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 10', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=False, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+
+    def test_case11(self):
+        """Test Case 11
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback : None
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 11', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=None, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ABNORMAL_TERMINATION, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+
+    def test_case12(self):
+        """Test Case 12
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 12', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=True, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_PENDING,
+                            RES_PENDING))
+
+        sess.close()
+
+    def test_case13(self):
+        """Test Case 13
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : None
+        </comment-en>
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 13', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=False, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+    def test_case14(self):
+        """Test Case 14
+          - Job Group : Abnormal termination
+          - Job : First Job Abnormal termination
+          - Rollback: None
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 14', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(False, True, True), rollback=None, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ABNORMAL_TERMINATION, RES_PENDING, RES_PENDING))
+
+        sess.close()
+
+    def test_case15(self):
+        """Test Case 15
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 15', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=True, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION, 
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION, 
+                            RES_PENDING))
+        sess.close()
+
+    def test_case16(self):
+        """Test Case 16
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 16', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=False, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_ABEND, RES_ROLLBACK_ABEND, RES_PENDING))
+
+        sess.close()
+
+
+    def test_case17(self):
+        """Test Case 17
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: None
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 17', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=None, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+              job=(RES_NORMAL_END, RES_ABNORMAL_TERMINATION, RES_PENDING))
+
+        sess.close()
+
+    def test_case18(self):
+        """Test Case 9
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 18', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=True, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_PENDING))
+
+        sess.close()
+
+    def test_case19(self):
+        """Test Case 19
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 19', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=False, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, RES_ROLLBACK_ABEND, RES_PENDING))
+
+        sess.close()
+
+
+    def test_case20(self):
+        """Test Case 11
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback : None
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 20', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=None, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_NORMAL_END,
+                            RES_ABNORMAL_TERMINATION,
+                            RES_PENDING))
+
+        sess.close()
+
+
+    def test_case21(self):
+        """Test Case 21
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 21', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=True, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_PENDING))
+
+        sess.close()
+
+    def test_case22(self):
+        """Test Case 22
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case ス22', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=False, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, RES_ROLLBACK_ABEND, RES_PENDING))
+
+        sess.close()
+
+    def test_case23(self):
+        """Test Case 23
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: None
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'テストケース23', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, False, True), rollback=None, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_NORMAL_END, 
+                            RES_ABNORMAL_TERMINATION,
+                            RES_PENDING))
+
+        sess.close()
+
+    def test_case24(self):
+        """Test Case 24
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 24', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=True, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION, 
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION, 
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION))
+        sess.close()
+
+    def test_case25(self):
+        """Test Case 25
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 25', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=False, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION,
+                       job=(RES_ROLLBACK_ABEND, 
+                            RES_ROLLBACK_ABEND,
+                            RES_ROLLBACK_ABEND))
+
+        sess.close()
+
+
+    def test_case26(self):
+        """Test Case 26
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback: None
+          - Send Mail : Normal end
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 26', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=None, mail=True)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+              job=(RES_NORMAL_END, RES_NORMAL_END, RES_ABNORMAL_TERMINATION))
+
+        sess.close()
+
+    def test_case27(self):
+        """Test Case 27
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 27', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=True, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION))
+
+        sess.close()
+
+    def test_case28(self):
+        """Test Case 28
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 28', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=False, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, RES_ROLLBACK_ABEND, RES_ROLLBACK_ABEND))
+
+        sess.close()
+
+
+    def test_case29(self):
+        """Test Case 29
+          - Job Group : Abnormal termination
+          - Job : Second Job Abnormal termination
+          - Rollback : None
+          - Send Mail : Abnormal termination
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 29', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=None, mail=False)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_NORMAL_END,
+                            RES_NORMAL_END,
+                            RES_ABNORMAL_TERMINATION))
+
+        sess.close()
+
+
+    def test_case30(self):
+        """Test Case 30
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Normal end
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 30', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=True, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION,
+                            RES_ROLLBACK_SUCCESSFUL_COMPLETION))
+
+        sess.close()
+
+    def test_case31(self):
+        """Test Case 31
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: Abnormal termination
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 31', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=False, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_ROLLBACK_ABEND, 
+                            RES_ROLLBACK_ABEND, 
+                            RES_ROLLBACK_ABEND))
+
+        sess.close()
+
+    def test_case32(self):
+        """Test Case 32
+          - Job Group : Abnormal termination
+          - Job : Third Job Abnormal termination
+          - Rollback: None
+          - Send Mail : None
+        """
+        sess = self._db.get_session()
+        self.set_job(sess, 'Test Case 32', 'b942f21c-4039-e6e9-09dc-9685985a1b84',
+                     job=(True, True, False), rollback=None, mail=None)
+
+        self.run_job(sess)
+
+        self.check_job(sess, jg=RES_ABNORMAL_TERMINATION, 
+                       job=(RES_NORMAL_END, 
+                            RES_NORMAL_END,
+                            RES_ABNORMAL_TERMINATION))
+
+        sess.close()
+
+def worker_debug(db, jobgroup_id):
+    logger = logging.getLogger('pysilhouette.performer.worker')
+    session = db.get_session()
+    jg = jobgroup_findbyid(session, jobgroup_id)
+    
+    def po(msg):
+        logger.debug(str(msg))
+
+    def poc(msg, code):
+        import pysilhouette.db.model
+        if pysilhouette.db.model.RES_PENDING == str(code):
+            po(msg+' : Pending')
+        if pysilhouette.db.model.RES_RUNNING == str(code):
+            po(msg+' : Running')
+        if pysilhouette.db.model.RES_NORMAL_END == str(code):
+            po(msg+' : Normal end')
+        if pysilhouette.db.model.RES_ABNORMAL_TERMINATION == str(code):
+            po(msg+' : Abnormal termination')
+        if pysilhouette.db.model.RES_ROLLBACK == str(code):
+            po(msg+' : Rollback running')
+        if pysilhouette.db.model.RES_ROLLBACK_ABEND == str(code):
+            po(msg+' : Rollback abend')
+        if pysilhouette.db.model.RES_ROLLBACK_SUCCESSFUL_COMPLETION == str(code):
+            po(msg+' : Rollback successful completion')
+    def pc(uni):
+        if not uni:
+            return str(uni)
+        else:
+            return str(uni.encode('utf-8'))
+
+    # debug print
+    po('------------------------------')
+    po("Job Group ID=%s" % jg.id)
+    po("Target host ip address=%s" % str(jg.uniq_key))
+    po("Job Group name=%s" % pc(jg.name))
+    poc("Job Group status now=%s" % str(jg.status), jg.status)
+    po("Finish Command='%s'" % pc(jg.finish_command))
+    po('------------------------------')
+
+    jobs = job_findbyjobgroup_id(session, jg.id, False)
+    for j in jobs:
+        po("Job ID=%s" % str(j.id))
+        po("Job Name=%s" % pc(j.name))
+        po("Job Order=%s" % str(j.order))
+        po("Job Progress=%s" % str(j.progress))
+        po("Job Action Commaind='%s'" % pc(j.action_command))
+        po("Job Rollbakc Command='%s'"% pc(j.rollback_command))
+        poc("Job Status=%s" % str(j.status), str(j.status))
+        po("Job Action Exit Code='%s'" % str(j.action_exit_code))
+        po("Job Action Stdout='%s'" % pc(j.action_stdout))
+        po("Job Action Stderr='%s'" % pc(j.action_stderr))
+        po("Job Rollback Exit Code='%s'" % str(j.rollback_exit_code))
+        po("Job Rollback Stdout='%s'" % pc(j.rollback_stdout))
+        po("Job Rollback Stderr='%s'" % pc(j.rollback_stderr))
+        po('------------------------------')
+        
+    session.close()
+
+def test_setup(cf):
+    # --
+    db = Database(cf['database.url'], encoding='utf-8', convert_unicode=True)
+    reload_mappers(db.get_metadata())
+
+    db.get_metadata().drop_all()
+    db.get_metadata().create_all()
+
+    f_cmd = (u'python /root/repository/pysilhouette_svn/pysilhouette/job/sendmail.py'
+             u' --from="root@localhost"'
+             u' --to="root@localhost"'
+             u' --subject="test"'
+             u' --hostname="localhost"'
+             u' --port="25"'
+             u' --msg="%s"'
+             u' --charset="utf-8"')
+
+    session = db.get_session()
+    jg1 = JobGroup(u'get date', '172.16.0.123')
+    jg1.finish_command = f_cmd % jg1.name
+    jb1 = Job(u'get date','0',u'/bin/date error')
+    jb1.rollback_command = u'/bin/date'
+    jg1.jobs.append(jb1)
+    jg2 = JobGroup(u'get route', '172.16.0.123')
+    jg2.finish_command = f_cmd % jg2.name
+    jg2.jobs.append(Job(u'get route','1', u'/sbin/route'))
+    jg3 = JobGroup(u'print string', '172.16.0.123')
+    jg3.finish_command = f_cmd % jg3.name
+    jg3.jobs.append(Job(u'print string','2', u'/bin/echo test'))
+    session.add_all([jg1,jg2,jg3])
+    #session.add(jg1)
+    session.commit()
+
+    from pysilhouette.db.access import jobgroup_findbystatus
+    _m_jgs = jobgroup_findbystatus(session)
+    return db, session, _m_jgs
+
+
+class SuiteWorker(unittest.TestSuite):
+    def __init__(self):
+        #tests = ['test_case1']
+        tests = ['test_case1', 'test_case2', 'test_case3', 
+                 'test_case4', 'test_case5', 'test_case6',
+                 'test_case7', 'test_case8', 'test_case9',
+                 'test_case10', 'test_case11', 'test_case12',
+                 'test_case13', 'test_case14', 'test_case15',
+                 'test_case16', 'test_case17', 'test_case18',
+                 'test_case19', 'test_case20', 'test_case21',
+                 'test_case22', 'test_case23', 'test_case24', 
+                 'test_case25', 'test_case26', 'test_case27', 
+                 'test_case28', 'test_case29', 'test_case30', 
+                 'test_case31', 'test_case32']
+
+        unittest.TestSuite.__init__(self,map(TestWorker, tests))
+
+def all_suite_worker():
+    return unittest.TestSuite([SuiteWorker()])
+
+if __name__ == '__main__':
+    job_path = '/root/repository/pysilhouette_svn/job/'
+
+    os.environ['PYSILHOUETTE_CONF'] = '/etc/opt/pysilhouette/silhouette.conf'
+    cf = readconf(os.environ['PYSILHOUETTE_CONF'])
+    pysilhouette.log.reload_conf(cf["env.sys.log.conf.path"])
+
+    ok_f_cmd = (u'python /root/pysilhouette/pysilhouette/job/sendmail.py' 
+                u' --from="root@localhost"' 
+                u' --to="root@localhost"'
+                u' --subject="Results of Worker(%s)"' 
+                u' --hostname="localhost"' 
+                u' --port="25"'
+                u' --msg="%s"' 
+                u' --charset="utf-8"') 
+
+    ng_f_cmd = (u'python /root/pysilhouette/pysilhouette/job/sendmail_dummy.py' 
+                u' --from="root@localhost"' 
+                u' --to="root@localhost"' 
+                u' --subject="Results of Worker(%s)"' 
+                u' --hostname="localhost"' 
+                u' --port="25"'
+                u' --msg="%s"' 
+                u' --charset="utf-8"') 
+
+    unittest.TextTestRunner(verbosity=2).run(all_suite_worker())
diff --git a/pysilhouette/uniqkey.py b/pysilhouette/uniqkey.py
new file mode 100644 (file)
index 0000000..b366f03
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import sys
+import random
+
+UNIQ_TPL = '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x'
+
+def getuniqkey():
+    _r = []
+    for _i in xrange(0,16):
+        _r.append(random.randint(0, 255))
+    return UNIQ_TPL % tuple(_r)
+
+if __name__ == '__main__':
+    print >>sys.stdout, getuniqkey()
+    
+        
diff --git a/pysilhouette/util.py b/pysilhouette/util.py
new file mode 100644 (file)
index 0000000..265744c
--- /dev/null
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import os
+import pwd
+import grp
+import subprocess
+import time
+import logging
+
+def is_empty(s):
+    """is empty
+    @param s: string
+    @type s: str
+    """
+    if s and 0 < len(s.strip()):
+        return False
+    else:
+        return True
+
+def astrftime(tm):
+    return time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(tm))
+    
+def split_shell_command(cmd):
+    ret = []
+    if is_empty(cmd) is False:
+        vs =cmd.split(' ')
+        for v in vs:
+            v = v.strip()
+            if is_empty(v): continue
+            ret.append(v)
+    return ret
+
+def write_pidfile(fname, pid):
+    try:
+        fp = open(fname, 'w')
+        try:
+            fp.write('%d' % pid)
+            return True
+        finally:
+            fp.close()
+
+    except:
+        return False
+
+def read_pidfile(fname):
+    try:
+        fp = open(fname, 'r')
+        try:
+            return fp.read()
+        finally:
+            fp.close()
+    except:
+        return ''
+        
+
+def create_fifo(fname, user, group, perm):
+    """create fifo file.
+    @param fname: file name
+    @type fname: str
+    @param: user: username
+    @type: user: str
+    @param: group: groupname
+    @type: group: str
+    @param: perm: Permission - example) '0666'
+    @type: perm: str(4)
+    """
+    try:
+        perm8 = int(perm, 8)
+        os.mkfifo(fname, perm8)
+        os.chown(fname, pwd.getpwnam(user)[2], grp.getgrnam(group)[2])
+        os.chmod(fname, perm8)
+        return True
+    except OSError ,o:
+        return False
+
+def kill_proc(proc):
+    if proc and hasattr(os, 'kill'):
+        import signal
+        try:
+            os.kill(proc.pid, signal.SIGTERM)
+            return True
+        except:
+            try:
+                os.kill(proc.pid, signal.SIGKILL)
+            except:
+                return False        
+
+def popen(cmd, timeout, waittime, lang, limit=4096, job_id=None):
+
+    proc_info = {}
+
+    timeout = int(timeout)
+    waittime = int(waittime)
+    env = os.environ.copy()
+    env['LANG'] = lang
+    if not (job_id is None):
+        env['JOB_ID'] = str(job_id)
+
+    proc = subprocess.Popen(cmd,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE,
+                            #env=os.environ,
+                            env=env,
+                            shell=False,
+                            )
+
+    # parent process wait.
+    start_time = time.time()
+    while True:
+        r = proc.poll()
+        if r is None:
+            interval = int(time.time() - start_time)
+            if 0 < timeout and timeout < interval:
+                try:
+                    kill_proc(proc)
+                finally:
+                    break
+            time.sleep(waittime)
+        else:
+            break
+
+    stdout = stderr = ''
+    for x in proc.stdout:
+        stdout += x
+    for x in proc.stderr:
+        stderr += x
+
+    if stdout and limit < len(stdout):
+        proc_info['stdout'] = stdout[:limit]
+    else:
+        proc_info['stdout'] = stdout
+    if stderr and limit < len(stderr):
+        proc_info['stderr'] = stderr[:limit]
+    else:
+        proc_info['stderr'] = stderr
+
+    proc_info['pid'] = proc.pid
+    proc_info['r_code'] = r
+
+    return proc, proc_info
+
+def debug_popen(proc, proc_info):
+    logger = logging.getLogger('pysilhouette.popen')
+
+    #logger.debug("Command : %s" % cmd)
+    logger.debug("Sub process id. id=%s" % proc.pid)
+    logger.debug("stdout : %s" % proc_info['stdout'])
+    logger.debug("stderr : %s" % proc_info['stderr'])
+
+    if os.WIFSTOPPED(proc_info['r_code']) is True:
+        logger.debug('The process stopped. code=%s' % proc_info['r_code'])
+    if os.WIFSIGNALED(proc_info['r_code']) is True:
+        logger.debug('The process stopped by the signal. code=%s' % \
+                  proc_info['r_code'])
+    if os.WIFEXITED(proc_info['r_code']) is True:
+        logger.debug('The process stopped by the system call. code=%s' % \
+                  proc_info['r_code'])
+    logger.debug('Integer parameter passed to system call. parameter=%s' % \
+              os.WEXITSTATUS(proc_info['r_code']))
+    logger.debug('The process stopped by the signal no. no=%s' % \
+              os.WSTOPSIG(proc_info['r_code']))
+    logger.debug('The process finished by the signal no. no=%s' % \
+              os.WTERMSIG(proc_info['r_code']))
+
+def is_int(val):
+    try:
+        int(val)
+        return True
+    except:
+        return False
+
+if __name__ == '__main__':
+    #print popen(cmd='efdsfdsafdsafdsafdsafdsa', timeout=3, waittime=1, lang='C')
+    print popen(cmd='date', timeout=3, waittime=1, lang='C')
diff --git a/pysilhouette/worker.py b/pysilhouette/worker.py
new file mode 100644 (file)
index 0000000..e6dbe06
--- /dev/null
@@ -0,0 +1,330 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei@karesansui-project.info>
+"""
+
+import subprocess
+import os
+import traceback
+import logging
+
+import pysilhouette
+from pysilhouette.db import *
+from pysilhouette.db.model import *
+from pysilhouette.db.access import jobgroup_findbyid, \
+     job_findbyjobgroup_id, jobgroup_update, job_update, \
+     job_result_action, job_result_rollback
+
+from pysilhouette.util import popen, kill_proc, is_empty, split_shell_command
+
+class SilhouetteWorkerException(pysilhouette.SilhouetteException):
+    """Worker execution error.
+    """
+    pass
+
+class Worker:
+    """Worker Base class
+    """
+    
+    def __init__(self, cf, db, jobgroup_id):
+        self._cf = cf
+        self._db = db
+        self._jobgroup_id = jobgroup_id
+        self.logger = logging.getLogger('pysilhouette.performer.worker')
+    
+    def run(self):
+        try:
+            session = self._db.get_session()
+            self.logger.debug('Session was obtained from the database. - session=%s' % session)
+            self._m_jg = jobgroup_findbyid(session,
+                                               self._jobgroup_id,
+                                               self._cf['env.uniqkey'])
+            
+            if self._m_jg is None: return False
+            
+            jobgroup_update(session, self._m_jg, JOBGROUP_STATUS['RUN']) # JobGroup UPDATE
+            _m_jobs = job_findbyjobgroup_id(session, self._jobgroup_id, False) # order asc
+
+            # action
+            ret = False
+            err = False
+            try:
+                ret = self._action(session, _m_jobs)
+            except Exception, e:
+                self.logger.info('Failed to perform the job action. Exceptions are not expected. - jobgroup_id=%d : %s'
+                             % (self._jobgroup_id, ','.join(e.args)))
+                
+                jobgroup_update(session, self._m_jg, JOBGROUP_STATUS['APPERR'])
+                t_logger = logging.getLogger('pysilhouette_traceback')
+                t_logger.info(traceback.format_exc())
+                err = True
+
+            try:
+                if err is False:
+                    if ret is True:
+                        # normal
+                        jobgroup_update(session, self._m_jg, JOBGROUP_STATUS['OK'])
+                    else:
+                        # rollback
+                        jobgroup_update(session, self._m_jg, JOBGROUP_STATUS['NG']) # JobGroup UPDATE
+                        try:
+                            self._rollback(session, _m_jobs)
+                        except Exception, e:
+                            self.logger.info('Failed to perform a rollback. Exceptions are not expected. - jobgroup_id=%d : %s'
+                                         % (self._jobgroup_id, ','.join(e.args)))
+                            t_logger = logging.getLogger('pysilhouette_traceback')
+                            t_logger.info(traceback.format_exc())
+                        
+            finally:
+                # finish
+                try:
+                    self._finish()
+                except Exception, e:
+                    self.logger.info('Failed to perform the finish action. Exceptions are not expected. - jobgroup_id=%d : %s'
+                                 % (self._jobgroup_id, ','.join(e.args)))
+                    t_logger = logging.getLogger('pysilhouette_traceback')
+                    t_logger.info(traceback.format_exc())
+        finally:
+            self.logger.debug('close database session, session=%s' % session)
+            session.close()
+
+    def _action(self, session, m_jobs):
+        raise SilhouetteWorkerException('Please override this method.')
+    
+    def _rollback(self, session, m_jobs):
+        raise SilhouetteWorkerException('Please override this method.')
+
+    def _finish(self):
+        proc = None
+        proc_info = []
+        cmd = self._m_jg.finish_command
+
+        if is_empty(cmd):
+            self.logger.debug('finish command not running!!- jobgroup_id=%d' % (self._m_jg.id))
+            return False # No finish Command
+        else:
+            try:
+                self.logger.info('finish command running!! - jobgroup_id=%d : cmd=%s'
+                                  % (self._m_jg.id, cmd))
+
+                lcmd = split_shell_command(cmd)
+
+                if self.chk_whitelist(lcmd[0]):
+                    try:
+                        (proc, proc_info) = popen(lcmd,
+                                                  self._cf['job.popen.timeout'],
+                                                  self._cf['job.popen.waittime'],
+                                                  self._cf['job.popen.env.lang'],
+                                                  )
+                        self.logger.debug('Of commands executed stdout=%s' % proc_info['stdout'])
+                        self.logger.debug('Of commands executed stderr=%s' % proc_info['stderr'])
+                        
+                    except OSError, oe:
+                        self.logger.info('finish command system failed!! job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                        raise oe
+
+                    if proc_info['r_code'] == 0:
+                        self.logger.info('finish command successful!! - jobgroup_id=%d : cmd=%s'
+                                          % (self._m_jg.id, cmd))
+                    else:
+                        self.logger.info('finish command failed!! - jobgroup_id=%d : cmd=%s'
+                                      % (self._m_jg.id, cmd))
+                    return True
+
+                else:
+                    # whitelist
+                    self.logger.info('Tried to run the rollback command that is not registered in the whitelist. - jobgroup_id=%d : cmd=%s'
+                                      % (self._m_jg.id, cmd))
+                    
+            finally:
+                kill_proc(proc)
+
+
+
+    def chk_whitelist(self, cmd):
+        flag = self._cf['job.whitelist.flag'].strip()
+        
+        if is_empty(flag) is True:
+            self.logger.debug("Whitelist feature [OFF] - empty")
+            return True # Unconditional
+        
+        if flag != "1":
+            self.logger.debug("Whitelist feature [OFF]")
+            return True # Unconditional
+
+        self.logger.debug("Whitelist feature [ON]")
+        fp = open(self._cf['job.whitelist.path'], 'r')
+        try:
+            for line in fp.readlines():
+                if cmd.strip() == line.strip():
+                    return True
+        finally:
+            fp.close()
+            
+        return False
+
+class SimpleWorker(Worker):
+    """Sequential Worker Class
+    """
+
+    def _action(self, session, m_jobs):
+        ret = True
+        for m_job in m_jobs: # job(N) execute
+            job_update(session, m_job, ACTION_STATUS['RUN']) # Job UPDATE
+            proc = None
+            proc_info = []
+            try:
+                cmd = m_job.action_command
+                self.logger.info('action command running!!- jobgroup_id=%d : cmd=%s'
+                                  % (m_job.id, cmd))
+
+                lcmd = split_shell_command(cmd)
+                if self.chk_whitelist(lcmd[0]):
+                    try:
+                        (proc, proc_info) = popen(lcmd,
+                                                  self._cf['job.popen.timeout'],
+                                                  self._cf['job.popen.waittime'],
+                                                  self._cf['job.popen.env.lang'],
+                                                  m_job.STD_OUTPUT_LIMIT,
+                                                  m_job.id,
+                                                  )
+
+                        self.logger.debug('Of commands executed stdout=%s' % proc_info['stdout'])
+                        self.logger.debug('Of commands executed stderr=%s' % proc_info['stderr'])
+
+                    except OSError, oe:
+                        self.logger.info('action command system failed!! job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                        raise oe
+    
+                    job_result_action(session, m_job, proc_info) # Job result UPDATE
+                    if proc_info['r_code'] == 0: # Normal end
+                        self.logger.info('action command was successful!! job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                        job_update(session, m_job, ACTION_STATUS['OK']) # Job UPDATE
+                    else: # Abnormal termination
+                        self.logger.info('action command failed!! job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                        job_update(session, m_job, ACTION_STATUS['NG']) # Job UPDATE
+                        ret = False
+                        break
+                else:
+                    # whitelist error
+                    self.logger.info('Tried to run the action command that is not registered in the whitelist. job_id=%d : cmd=%s'
+                                      % (m_job.id, cmd))
+                    m_job.action_stderr = "Command is not registered to run the whitelist."
+                    job_update(session, m_job, ACTION_STATUS['WHITELIST']) # Job UPDATE
+                    ret = False
+                    break
+                    
+            finally:
+                kill_proc(proc)
+                
+        return ret
+    
+    def _rollback(self, session, m_jobs):
+        for m_job in m_jobs:
+
+            if m_job.is_rollback() and m_job.status in (ACTION_STATUS['RUN'],
+                                                        ACTION_STATUS['OK'],
+                                                        ACTION_STATUS['NG'],
+                                                        ACTION_STATUS['WHITELIST']):
+                # rollback exec
+                proc = None
+                proc_info = []
+                try:
+                    #cmd = m_job.action_command
+                    cmd = m_job.rollback_command
+                    self.logger.info('rollback command running!!- jobgroup_id=%d : cmd=%s'
+                                      % (m_job.id, cmd))
+
+                    lcmd = split_shell_command(cmd)
+                    
+                    if self.chk_whitelist(lcmd[0]):
+                        try:
+                            (proc, proc_info) = popen(lcmd,
+                                                      self._cf['job.popen.timeout'],
+                                                      self._cf['job.popen.waittime'],
+                                                      self._cf['job.popen.env.lang'],
+                                                      m_job.STD_OUTPUT_LIMIT,
+                                                      )
+
+                            self.logger.debug('Of commands executed stdout=%s' % proc_info['stdout'])
+                            self.logger.debug('Of commands executed stderr=%s' % proc_info['stderr'])
+
+                        except OSError, oe:
+                            self.logger.info('rollback command system failed!! job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                            raise oe
+                    
+                        job_result_rollback(session, m_job, proc_info) # Job result UPDATE
+                        if proc_info['r_code'] == 0: # Normal end
+                            self.logger.info('rollback command was successful!! job_id=%d : cmd=%s'
+                                              % (m_job.id, cmd))
+                            job_update(session, m_job, ROLLBACK_STATUS['OK']) # Job UPDATE
+                        else: # Abnormal termination
+                            self.logger.info('rollback command failed!! job_id=%d : cmd=%s'
+                                              % (m_job.id, cmd))
+                            job_update(session, m_job, ROLLBACK_STATUS['NG']) # Job UPDATE
+
+                    else:
+                        # whitelist error
+                        self.logger.info('Tried to run the rollback command that is not registered in the whitelist. job_id=%d : cmd=%s'
+                                          % (m_job.id, cmd))
+                        m_job.rollback_stderr = "Command is not registered to run the whitelist."
+                        job_update(session, m_job, ROLLBACK_STATUS['WHITELIST']) # Job UPDATE
+
+                finally:
+                    kill_proc(proc)
+            else:
+                self.logger.debug('Does not rollback the process. - job_id=%d : status=%s'
+                                  % (m_job.id, m_job.status))
+
+if __name__ == '__main__':
+    """Testing
+    """
+    import pysilhouette.tests.testworker
+    # dev start
+    _env = os.environ
+    _env['PYSILHOUETTE_CONF'] = '/etc/opt/pysilhouette/silhouette.conf'
+    # dev end
+
+    # init
+    from pysilhouette.prep import readconf 
+    cf = readconf(os.environ['PYSILHOUETTE_CONF'])
+    pysilhouette.cf = pysilhouette.prep.readconf(os.environ['PYSILHOUETTE_CONF'])
+    pysilhouette.log.reload_conf(pysilhouette.cf["env.sys.log.conf.path"])
+
+    (db, session, _m_jgs) = pysilhouette.tests.testworker.test_setup(cf)
+    # run
+    for _m_jg in _m_jgs:
+        _w = SimpleWorker(db, _m_jg.id)
+        _w.run()
+        pysilhouette.tests.testworker.worker_debug(db, _m_jg.id)
+    session.close()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..55350b4
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,7 @@
+[global]
+verbose=1
+[install]
+optimize = 1
+[bdist_rpm]
+release=1
+doc_files=doc tool example
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..eb21854
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# 
+
+"""
+@author: Kei Funagayama <kei.funagayama@hde.co.jp>
+"""
+
+from distutils.core import setup
+import glob
+import os
+import sys
+# DISTUTILS_DEBUG=1
+from pysilhouette import __app__, __version__, __release__
+
+__prog__ = 'silhouette'
+__progd__='%sd' % __prog__
+
+setup(name=__app__,
+    version= "%s.%s" % (__version__, __release__),
+    description='Pysilhouette is an application running in the background system.',
+    long_description="""Pysilhouette is an application running in the background system.
+    A system executes the job command registered into the database.
+    100% Pure Python.""",
+    maintainer='HDE Package Maintainer',
+    maintainer_email='info@hde.co.jp',
+    url='http://sourceforge.jp/projects/pysilhouette/',
+    download_url='',
+    packages=['pysilhouette',
+              'pysilhouette.db',
+              'pysilhouette.tests',
+              ],
+    py_modules=[],
+    scripts=[],
+    license='The MIT License',
+    keywords='',
+    platforms='Linux',
+    classifiers=['Environment :: No Input/Output (Daemon)',
+                 'License :: OSI Approved :: MIT License',
+                 'Natural Language :: Japanese',
+                 'Programming Language :: Python :: 2.4',
+                 ],
+)
+
diff --git a/tool/cleanupdb.py b/tool/cleanupdb.py
new file mode 100644 (file)
index 0000000..e8515bc
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei.funagayama@hde.co.jp>
+"""
+
+import sys
+import os
+import logging
+
+from pysilhouette.prep import getopts, readconf, chkopts
+from pysilhouette.db import Database, reload_mappers
+
+def main():
+    (opts, args) = getopts()
+    if chkopts(opts) is True:
+        return 1
+    
+    try:
+        opts.config = os.path.abspath(opts.config)
+    except AttributeError, e:
+        print >>sys.stderr, 'No configuration file path.'
+        return 1
+    
+    cf = readconf(opts.config)
+    if cf is None:
+        print >>sys.stderr, 'Failed to load the config file.'
+        return 1
+    
+    try:
+        db = Database(cf['database.url'],
+                      encoding="utf-8",
+                      convert_unicode=True,
+                      assert_unicode=False, # product
+                      #assert_unicode='warn', # dev
+                      #echo = opts.verbose,
+                      #echo_pool = opts.verbose,
+                      echo=True,
+                      echo_pool=True
+                      )
+
+        reload_mappers(db.get_metadata())
+
+    except Exception, e:
+        print >>sys.stderr, 'Initializing a database error'
+        raise
+    
+    try:
+        db.get_metadata().drop_all()
+        db.get_metadata().create_all()
+        print >>sys.stdout, 'Cleanup Database [OK]'
+    except Exception,e:
+        print >>sys.stderr, 'database drop and create error.'
+        raise
+
+
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/tool/epydoc.sh b/tool/epydoc.sh
new file mode 100644 (file)
index 0000000..f0aa354
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+name=pysilhouette
+export PYTHONPATH=${PYTHONPATH}:/opt/pysilhouette/lib/python
+
+script_dir=`dirname $0`
+pushd $script_dir >/dev/null 2>&1
+# shell directory.
+script_dir=`pwd`
+popd >/dev/null 2>&1
+
+epydoc_config=${script_dir}/../doc/epydoc.cfg
+
+
+target=/var/www/html/${name}-doc
+
+if [ -e ${target} ]; then
+  rm -fr ${target}
+fi
+mkdir -p ${target}
+
+epydoc --config ${epydoc_config}
diff --git a/tool/setdummy.py b/tool/setdummy.py
new file mode 100644 (file)
index 0000000..3f5800c
--- /dev/null
@@ -0,0 +1,99 @@
+]#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of Pysilhouette.
+#
+# Copyright (c) 2009 HDE, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+@author: Kei Funagayama <kei.funagayama@hde.co.jp>
+"""
+
+import sys
+import os
+import logging
+
+from pysilhouette.prep import getopts, readconf, chkopts
+from pysilhouette.db import Database, reload_mappers
+from pysilhouette.db.model import JobGroup, Job
+
+NUM = 10
+
+def main():
+    (opts, args) = getopts()
+    if chkopts(opts) is True:
+        return 1
+    
+    try:
+        opts.config = os.path.abspath(opts.config)
+    except AttributeError, e:
+        print >>sys.stderr, 'No configuration file path.'
+        return 1
+    
+    cf = readconf(opts.config)
+    if cf is None:
+        print >>sys.stderr, 'Failed to load the config file.'
+        return 1
+    
+    try:
+        db = Database(cf['database.url'],
+                      encoding="utf-8",
+                      convert_unicode=True,
+                      assert_unicode=False, # product
+                      #assert_unicode='warn', # dev
+                      #echo = opts.verbose,
+                      #echo_pool = opts.verbose,
+                      echo=True,
+                      echo_pool=True
+                      )
+        
+        reload_mappers(db.get_metadata())
+        session = db.get_session()
+        
+    except Exception, e:
+        print >>sys.stderr, 'Initializing a database error'
+        raise
+    
+    try:
+        jgs = []
+        for i in range(NUM):
+            jg_name = u'JobGroup-%d' % i
+            jg_ukey = unicode(cf['env.uniqkey'], "utf-8") 
+            j_name = u'Job-%d' % i
+            j_order = i
+            j_cmd = u'/bin/echo num=%d' % i
+            jg = JobGroup(jg_name, jg_ukey)
+            jg.jobs.append(
+                Job(j_name, j_order, j_cmd))
+            jgs.append(jg)
+            
+        session.add_all(jgs)
+        session.commit()
+        session.close()
+        print >>sys.stdout, 'Insert JobGroup and Job. num=%d [OK]' % NUM
+    except:
+        print >>sys.stderr, 'Failed to add JobGroup and Job.'
+        raise
+            
+if __name__ == '__main__':
+    sys.exit(main())
+