[PATCH V3] [Wok] Use system's nginx proxy service.

From: Paulo Vital <pvital@linux.vnet.ibm.com> This patch-set works on Fedora 23 and 24, RHEL 7.2, Ubuntu 16.04, Debian 8.5 and OpenSUSE 42.1 Just pay attention to follow current steps reported in Troubleshooting wiki page of Wok: https://github.com/kimchi-project/wok/blob/master/docs/troubleshooting.md V3: - removed unnecessary semanage command. V2: - fixed PEP8 errors V1: This patch removes the code that executes a dedicated nginx proxy, making Wok to use the system's nginx service. This is a requirement to make Wok acceptable in community repositories. It also make sure that a Wok executed from path different than installed (from a cloned and builded source code, for example) will create a symbolic link in system's nginx config dir to the running configuration file. This patch solves part of issue #25 Paulo Vital (1): Use system's nginx proxy service. src/nginx/wok.conf.in | 95 +++++++++++++++++++++------------------------------ src/wok/config.py.in | 6 +++- src/wok/i18n.py | 2 ++ src/wok/proxy.py | 28 ++++++++------- src/wok/server.py | 6 +--- 5 files changed, 63 insertions(+), 74 deletions(-) -- 2.7.4

From: Paulo Vital <pvital@linux.vnet.ibm.com> This patch removes the code that executes a dedicated nginx proxy, making Wok to use the system's nginx service. This is a requirement to make Wok acceptable in community repositories. It also make sure that a Wok executed from path different than installed (from a cloned and builded source code, for example) will create a symbolic link in system's nginx config dir to the running configuration file. This patch solves part of issue #25 Signed-off-by: Paulo Vital <pvital@linux.vnet.ibm.com> --- src/nginx/wok.conf.in | 95 +++++++++++++++++++++------------------------------ src/wok/config.py.in | 6 +++- src/wok/i18n.py | 2 ++ src/wok/proxy.py | 28 ++++++++------- src/wok/server.py | 6 +--- 5 files changed, 63 insertions(+), 74 deletions(-) diff --git a/src/nginx/wok.conf.in b/src/nginx/wok.conf.in index cb05e4d..823d94d 100644 --- a/src/nginx/wok.conf.in +++ b/src/nginx/wok.conf.in @@ -22,71 +22,54 @@ # This is a template file to be used to generate a nginx # proxy config file at wokd script. -user ${user}; -worker_processes 1; +client_max_body_size ${max_body_size}k; -error_log /var/log/nginx/error.log; +# Timeout set to 10 minutes to avoid the 504 Gateway Timeout +# when Wok is processing a request. +proxy_connect_timeout 600; +proxy_send_timeout 600; +proxy_read_timeout 600; +send_timeout 600; -events { - worker_connections 1024; +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; } -http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; +upstream websocket { + server 127.0.0.1:${websockets_port}; +} - client_max_body_size ${max_body_size}k; +server { + listen ${host_addr}:${proxy_ssl_port} ssl; - # Timeout set to 10 minutes to avoid the 504 Gateway Timeout - # when Wok is processing a request. - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; + ssl_certificate ${cert_pem}; + ssl_certificate_key ${cert_key}; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; + ssl_prefer_server_ciphers on; + ssl_dhparam ${dhparams_pem}; + ssl_session_timeout ${session_timeout}m; - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; - upstream websocket { - server 127.0.0.1:${websockets_port}; + location / { + proxy_pass http://127.0.0.1:${cherrypy_port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; } - server { - listen ${host_addr}:${proxy_ssl_port} ssl; - - ssl_certificate ${cert_pem}; - ssl_certificate_key ${cert_key}; - ssl_protocols TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; - ssl_prefer_server_ciphers on; - ssl_dhparam ${dhparams_pem}; - ssl_session_timeout ${session_timeout}m; - - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - - location / { - proxy_pass http://127.0.0.1:${cherrypy_port}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; - } - - location /websockify { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } + location /websockify { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; } - ${http_config} } + +${http_config} diff --git a/src/wok/config.py.in b/src/wok/config.py.in index 1ca6f73..d0bca9b 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -76,9 +76,10 @@ class Paths(object): self.prefix = self.get_prefix() self.installed = (self.prefix == '@pkgdatadir@') self.ui_dir = self.add_prefix('ui') + self.sys_nginx_conf_dir = '@sysconfdir@/nginx/conf.d' if self.installed: - self.nginx_conf_dir = '@sysconfdir@/nginx/conf.d' + self.nginx_conf_dir = self.sys_nginx_conf_dir self.state_dir = '@localstatedir@/lib/wok' self.log_dir = '@localstatedir@/log/wok' self.conf_dir = '@sysconfdir@/wok' @@ -119,6 +120,9 @@ class Paths(object): def get_template_path(self, resource): return os.path.join(self.ui_dir, 'pages/%s.tmpl' % resource) + def is_wok_installed(self): + return self.installed + paths = Paths() diff --git a/src/wok/i18n.py b/src/wok/i18n.py index d6cb17c..33107ee 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -55,6 +55,8 @@ messages = { "WOKUTILS0004E": _("Invalid data value '%(value)s'"), "WOKUTILS0005E": _("Invalid data unit '%(unit)s'"), + "WOKPROXY0001E": _("Unable to (re)start system's nginx.service. Details: '%(error)s'"), + # These messages (ending with L) are for user log purposes "WOKCOL0001L": _("Request made on collection"), "WOKRES0001L": _("Request made on resource"), diff --git a/src/wok/proxy.py b/src/wok/proxy.py index a74e88a..cf978ad 100644 --- a/src/wok/proxy.py +++ b/src/wok/proxy.py @@ -26,11 +26,12 @@ import os import pwd -import subprocess from string import Template from wok import sslcert from wok.config import paths +from wok.exception import OperationFailed +from wok.utils import run_command HTTP_CONFIG = """ @@ -110,18 +111,21 @@ def _create_proxy_config(options): config_file.write(data) config_file.close() + # If not running from the installed path (from a cloned and builded source + # code), create a symbolic link in system's dir to prevent errors on read + # SSL certifications. + if not paths.is_wok_installed(): + dst = os.path.join(paths.sys_nginx_conf_dir, "wok.conf") + if os.path.isfile(dst) or os.path.islink(dst): + os.remove(dst) + os.symlink(os.path.join(nginx_config_dir, "wok.conf"), dst) + def start_proxy(options): """Start nginx reverse proxy.""" _create_proxy_config(options) - nginx_config_dir = paths.nginx_conf_dir - config_file = "%s/wok.conf" % nginx_config_dir - cmd = ['nginx', '-c', config_file] - subprocess.call(cmd) - - -def terminate_proxy(): - """Stop nginx process.""" - config_file = "%s/wok.conf" % paths.nginx_conf_dir - term_proxy_cmd = ['nginx', '-s', 'stop', '-c', config_file] - subprocess.call(term_proxy_cmd) + # Restart system's nginx service to reload wok configuration + cmd = ['systemctl', 'restart', 'nginx.service'] + output, error, retcode = run_command(cmd, silent=True) + if retcode != 0: + raise OperationFailed('WOKPROXY0001E', {'error': error}) diff --git a/src/wok/server.py b/src/wok/server.py index 8a02596..b1185e3 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -33,7 +33,7 @@ from wok.config import config as configParser from wok.config import paths, PluginConfig, WokConfig from wok.control import sub_nodes from wok.model import model -from wok.proxy import start_proxy, terminate_proxy +from wok.proxy import start_proxy from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler @@ -180,10 +180,6 @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), config=self.configObj) self._load_plugins(options) - - # Terminate proxy when cherrypy server is terminated - cherrypy.engine.subscribe('exit', terminate_proxy) - cherrypy.lib.sessions.init() def _load_plugins(self, options): -- 2.7.4

Reviewed-by: Daniel Barboza <danielhb@linux.vnet.ibm.com> Tested-by: Daniel Barboza <danielhb@linux.vnet.ibm.com> Note: I had to do a few additional steps to make it work by source code running in F23. SELinux complained about nginx accessing a file in my home dir. I had to issue the following commands: $ ausearch -c 'nginx' --raw | audit2allow -M my-nginx $ semodule -X 300 -i my-nginx.pp For this patch to properly work. This step is not needed when installing WoK by the RPM. On 07/18/2016 06:59 PM, pvital@linux.vnet.ibm.com wrote:
From: Paulo Vital <pvital@linux.vnet.ibm.com>
This patch removes the code that executes a dedicated nginx proxy, making Wok to use the system's nginx service. This is a requirement to make Wok acceptable in community repositories.
It also make sure that a Wok executed from path different than installed (from a cloned and builded source code, for example) will create a symbolic link in system's nginx config dir to the running configuration file.
This patch solves part of issue #25
Signed-off-by: Paulo Vital <pvital@linux.vnet.ibm.com> --- src/nginx/wok.conf.in | 95 +++++++++++++++++++++------------------------------ src/wok/config.py.in | 6 +++- src/wok/i18n.py | 2 ++ src/wok/proxy.py | 28 ++++++++------- src/wok/server.py | 6 +--- 5 files changed, 63 insertions(+), 74 deletions(-)
diff --git a/src/nginx/wok.conf.in b/src/nginx/wok.conf.in index cb05e4d..823d94d 100644 --- a/src/nginx/wok.conf.in +++ b/src/nginx/wok.conf.in @@ -22,71 +22,54 @@ # This is a template file to be used to generate a nginx # proxy config file at wokd script.
-user ${user}; -worker_processes 1; +client_max_body_size ${max_body_size}k;
-error_log /var/log/nginx/error.log; +# Timeout set to 10 minutes to avoid the 504 Gateway Timeout +# when Wok is processing a request. +proxy_connect_timeout 600; +proxy_send_timeout 600; +proxy_read_timeout 600; +send_timeout 600;
-events { - worker_connections 1024; +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; }
-http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; +upstream websocket { + server 127.0.0.1:${websockets_port}; +}
- client_max_body_size ${max_body_size}k; +server { + listen ${host_addr}:${proxy_ssl_port} ssl;
- # Timeout set to 10 minutes to avoid the 504 Gateway Timeout - # when Wok is processing a request. - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; + ssl_certificate ${cert_pem}; + ssl_certificate_key ${cert_key}; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; + ssl_prefer_server_ciphers on; + ssl_dhparam ${dhparams_pem}; + ssl_session_timeout ${session_timeout}m;
- map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block";
- upstream websocket { - server 127.0.0.1:${websockets_port}; + location / { + proxy_pass http://127.0.0.1:${cherrypy_port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; }
- server { - listen ${host_addr}:${proxy_ssl_port} ssl; - - ssl_certificate ${cert_pem}; - ssl_certificate_key ${cert_key}; - ssl_protocols TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; - ssl_prefer_server_ciphers on; - ssl_dhparam ${dhparams_pem}; - ssl_session_timeout ${session_timeout}m; - - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - - location / { - proxy_pass http://127.0.0.1:${cherrypy_port}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; - } - - location /websockify { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } + location /websockify { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; } - ${http_config} } + +${http_config} diff --git a/src/wok/config.py.in b/src/wok/config.py.in index 1ca6f73..d0bca9b 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -76,9 +76,10 @@ class Paths(object): self.prefix = self.get_prefix() self.installed = (self.prefix == '@pkgdatadir@') self.ui_dir = self.add_prefix('ui') + self.sys_nginx_conf_dir = '@sysconfdir@/nginx/conf.d'
if self.installed: - self.nginx_conf_dir = '@sysconfdir@/nginx/conf.d' + self.nginx_conf_dir = self.sys_nginx_conf_dir self.state_dir = '@localstatedir@/lib/wok' self.log_dir = '@localstatedir@/log/wok' self.conf_dir = '@sysconfdir@/wok' @@ -119,6 +120,9 @@ class Paths(object): def get_template_path(self, resource): return os.path.join(self.ui_dir, 'pages/%s.tmpl' % resource)
+ def is_wok_installed(self): + return self.installed +
paths = Paths()
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index d6cb17c..33107ee 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -55,6 +55,8 @@ messages = { "WOKUTILS0004E": _("Invalid data value '%(value)s'"), "WOKUTILS0005E": _("Invalid data unit '%(unit)s'"),
+ "WOKPROXY0001E": _("Unable to (re)start system's nginx.service. Details: '%(error)s'"), + # These messages (ending with L) are for user log purposes "WOKCOL0001L": _("Request made on collection"), "WOKRES0001L": _("Request made on resource"), diff --git a/src/wok/proxy.py b/src/wok/proxy.py index a74e88a..cf978ad 100644 --- a/src/wok/proxy.py +++ b/src/wok/proxy.py @@ -26,11 +26,12 @@
import os import pwd -import subprocess from string import Template
from wok import sslcert from wok.config import paths +from wok.exception import OperationFailed +from wok.utils import run_command
HTTP_CONFIG = """ @@ -110,18 +111,21 @@ def _create_proxy_config(options): config_file.write(data) config_file.close()
+ # If not running from the installed path (from a cloned and builded source + # code), create a symbolic link in system's dir to prevent errors on read + # SSL certifications. + if not paths.is_wok_installed(): + dst = os.path.join(paths.sys_nginx_conf_dir, "wok.conf") + if os.path.isfile(dst) or os.path.islink(dst): + os.remove(dst) + os.symlink(os.path.join(nginx_config_dir, "wok.conf"), dst) +
def start_proxy(options): """Start nginx reverse proxy.""" _create_proxy_config(options) - nginx_config_dir = paths.nginx_conf_dir - config_file = "%s/wok.conf" % nginx_config_dir - cmd = ['nginx', '-c', config_file] - subprocess.call(cmd) - - -def terminate_proxy(): - """Stop nginx process.""" - config_file = "%s/wok.conf" % paths.nginx_conf_dir - term_proxy_cmd = ['nginx', '-s', 'stop', '-c', config_file] - subprocess.call(term_proxy_cmd) + # Restart system's nginx service to reload wok configuration + cmd = ['systemctl', 'restart', 'nginx.service'] + output, error, retcode = run_command(cmd, silent=True) + if retcode != 0: + raise OperationFailed('WOKPROXY0001E', {'error': error}) diff --git a/src/wok/server.py b/src/wok/server.py index 8a02596..b1185e3 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -33,7 +33,7 @@ from wok.config import config as configParser from wok.config import paths, PluginConfig, WokConfig from wok.control import sub_nodes from wok.model import model -from wok.proxy import start_proxy, terminate_proxy +from wok.proxy import start_proxy from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler @@ -180,10 +180,6 @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), config=self.configObj) self._load_plugins(options) - - # Terminate proxy when cherrypy server is terminated - cherrypy.engine.subscribe('exit', terminate_proxy) - cherrypy.lib.sessions.init()
def _load_plugins(self, options):

On 07/18/2016 06:59 PM, pvital@linux.vnet.ibm.com wrote:
From: Paulo Vital <pvital@linux.vnet.ibm.com>
This patch removes the code that executes a dedicated nginx proxy, making Wok to use the system's nginx service. This is a requirement to make Wok acceptable in community repositories.
It also make sure that a Wok executed from path different than installed (from a cloned and builded source code, for example) will create a symbolic link in system's nginx config dir to the running configuration file.
This patch solves part of issue #25
Signed-off-by: Paulo Vital <pvital@linux.vnet.ibm.com> --- src/nginx/wok.conf.in | 95 +++++++++++++++++++++------------------------------ src/wok/config.py.in | 6 +++- src/wok/i18n.py | 2 ++ src/wok/proxy.py | 28 ++++++++------- src/wok/server.py | 6 +--- 5 files changed, 63 insertions(+), 74 deletions(-)
diff --git a/src/nginx/wok.conf.in b/src/nginx/wok.conf.in index cb05e4d..823d94d 100644 --- a/src/nginx/wok.conf.in +++ b/src/nginx/wok.conf.in @@ -22,71 +22,54 @@ # This is a template file to be used to generate a nginx # proxy config file at wokd script.
-user ${user}; -worker_processes 1; +client_max_body_size ${max_body_size}k;
-error_log /var/log/nginx/error.log;
+# Timeout set to 10 minutes to avoid the 504 Gateway Timeout +# when Wok is processing a request.
The session timeout is configurable in the wok config file so you should not assume 10 minutes. You will need to get the timeout value and do the right count to set the nginx config file.
+proxy_connect_timeout 600; +proxy_send_timeout 600; +proxy_read_timeout 600; +send_timeout 600;
-events { - worker_connections 1024; +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; }
-http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; +upstream websocket { + server 127.0.0.1:${websockets_port}; +}
- client_max_body_size ${max_body_size}k; +server { + listen ${host_addr}:${proxy_ssl_port} ssl;
- # Timeout set to 10 minutes to avoid the 504 Gateway Timeout - # when Wok is processing a request. - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; + ssl_certificate ${cert_pem}; + ssl_certificate_key ${cert_key}; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; + ssl_prefer_server_ciphers on; + ssl_dhparam ${dhparams_pem}; + ssl_session_timeout ${session_timeout}m;
- map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block";
- upstream websocket { - server 127.0.0.1:${websockets_port}; + location / { + proxy_pass http://127.0.0.1:${cherrypy_port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; }
- server { - listen ${host_addr}:${proxy_ssl_port} ssl; - - ssl_certificate ${cert_pem}; - ssl_certificate_key ${cert_key}; - ssl_protocols TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; - ssl_prefer_server_ciphers on; - ssl_dhparam ${dhparams_pem}; - ssl_session_timeout ${session_timeout}m; - - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - - location / { - proxy_pass http://127.0.0.1:${cherrypy_port}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; - } - - location /websockify { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } + location /websockify { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; } - ${http_config} } + +${http_config} diff --git a/src/wok/config.py.in b/src/wok/config.py.in index 1ca6f73..d0bca9b 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -76,9 +76,10 @@ class Paths(object): self.prefix = self.get_prefix() self.installed = (self.prefix == '@pkgdatadir@') self.ui_dir = self.add_prefix('ui') + self.sys_nginx_conf_dir = '@sysconfdir@/nginx/conf.d'
if self.installed: - self.nginx_conf_dir = '@sysconfdir@/nginx/conf.d' + self.nginx_conf_dir = self.sys_nginx_conf_dir self.state_dir = '@localstatedir@/lib/wok' self.log_dir = '@localstatedir@/log/wok' self.conf_dir = '@sysconfdir@/wok' @@ -119,6 +120,9 @@ class Paths(object): def get_template_path(self, resource): return os.path.join(self.ui_dir, 'pages/%s.tmpl' % resource)
+ def is_wok_installed(self): + return self.installed +
In other part of the code, we are accessing self.installed directly! Any special reason to create this new function? Seems like too Java huh
paths = Paths()
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index d6cb17c..33107ee 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -55,6 +55,8 @@ messages = { "WOKUTILS0004E": _("Invalid data value '%(value)s'"), "WOKUTILS0005E": _("Invalid data unit '%(unit)s'"),
+ "WOKPROXY0001E": _("Unable to (re)start system's nginx.service. Details: '%(error)s'"), + # These messages (ending with L) are for user log purposes "WOKCOL0001L": _("Request made on collection"), "WOKRES0001L": _("Request made on resource"), diff --git a/src/wok/proxy.py b/src/wok/proxy.py index a74e88a..cf978ad 100644 --- a/src/wok/proxy.py +++ b/src/wok/proxy.py @@ -26,11 +26,12 @@
import os import pwd -import subprocess from string import Template
from wok import sslcert from wok.config import paths +from wok.exception import OperationFailed +from wok.utils import run_command
HTTP_CONFIG = """ @@ -110,18 +111,21 @@ def _create_proxy_config(options): config_file.write(data) config_file.close()
+ # If not running from the installed path (from a cloned and builded source + # code), create a symbolic link in system's dir to prevent errors on read + # SSL certifications. + if not paths.is_wok_installed(): + dst = os.path.join(paths.sys_nginx_conf_dir, "wok.conf") + if os.path.isfile(dst) or os.path.islink(dst): + os.remove(dst) + os.symlink(os.path.join(nginx_config_dir, "wok.conf"), dst) +
def start_proxy(options): """Start nginx reverse proxy.""" _create_proxy_config(options) - nginx_config_dir = paths.nginx_conf_dir - config_file = "%s/wok.conf" % nginx_config_dir - cmd = ['nginx', '-c', config_file] - subprocess.call(cmd) - - -def terminate_proxy(): - """Stop nginx process.""" - config_file = "%s/wok.conf" % paths.nginx_conf_dir - term_proxy_cmd = ['nginx', '-s', 'stop', '-c', config_file] - subprocess.call(term_proxy_cmd) + # Restart system's nginx service to reload wok configuration + cmd = ['systemctl', 'restart', 'nginx.service'] + output, error, retcode = run_command(cmd, silent=True) + if retcode != 0: + raise OperationFailed('WOKPROXY0001E', {'error': error}) diff --git a/src/wok/server.py b/src/wok/server.py index 8a02596..b1185e3 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -33,7 +33,7 @@ from wok.config import config as configParser from wok.config import paths, PluginConfig, WokConfig from wok.control import sub_nodes from wok.model import model -from wok.proxy import start_proxy, terminate_proxy +from wok.proxy import start_proxy from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler @@ -180,10 +180,6 @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), config=self.configObj) self._load_plugins(options) - - # Terminate proxy when cherrypy server is terminated - cherrypy.engine.subscribe('exit', terminate_proxy) - cherrypy.lib.sessions.init()
def _load_plugins(self, options):

On Jul 21 11:11AM, Aline Manera wrote:
On 07/18/2016 06:59 PM, pvital@linux.vnet.ibm.com wrote:
From: Paulo Vital <pvital@linux.vnet.ibm.com>
This patch removes the code that executes a dedicated nginx proxy, making Wok to use the system's nginx service. This is a requirement to make Wok acceptable in community repositories.
It also make sure that a Wok executed from path different than installed (from a cloned and builded source code, for example) will create a symbolic link in system's nginx config dir to the running configuration file.
This patch solves part of issue #25
Signed-off-by: Paulo Vital <pvital@linux.vnet.ibm.com> --- src/nginx/wok.conf.in | 95 +++++++++++++++++++++------------------------------ src/wok/config.py.in | 6 +++- src/wok/i18n.py | 2 ++ src/wok/proxy.py | 28 ++++++++------- src/wok/server.py | 6 +--- 5 files changed, 63 insertions(+), 74 deletions(-)
diff --git a/src/nginx/wok.conf.in b/src/nginx/wok.conf.in index cb05e4d..823d94d 100644 --- a/src/nginx/wok.conf.in +++ b/src/nginx/wok.conf.in @@ -22,71 +22,54 @@ # This is a template file to be used to generate a nginx # proxy config file at wokd script.
-user ${user}; -worker_processes 1; +client_max_body_size ${max_body_size}k;
-error_log /var/log/nginx/error.log;
+# Timeout set to 10 minutes to avoid the 504 Gateway Timeout +# when Wok is processing a request.
The session timeout is configurable in the wok config file so you should not assume 10 minutes. You will need to get the timeout value and do the right count to set the nginx config file.
Interesting!!! The current code has this values hardcoded. I'll change this.
+proxy_connect_timeout 600; +proxy_send_timeout 600; +proxy_read_timeout 600; +send_timeout 600;
-events { - worker_connections 1024; +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; }
-http { - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - sendfile on; +upstream websocket { + server 127.0.0.1:${websockets_port}; +}
- client_max_body_size ${max_body_size}k; +server { + listen ${host_addr}:${proxy_ssl_port} ssl;
- # Timeout set to 10 minutes to avoid the 504 Gateway Timeout - # when Wok is processing a request. - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; + ssl_certificate ${cert_pem}; + ssl_certificate_key ${cert_key}; + ssl_protocols TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; + ssl_prefer_server_ciphers on; + ssl_dhparam ${dhparams_pem}; + ssl_session_timeout ${session_timeout}m;
- map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block";
- upstream websocket { - server 127.0.0.1:${websockets_port}; + location / { + proxy_pass http://127.0.0.1:${cherrypy_port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; }
- server { - listen ${host_addr}:${proxy_ssl_port} ssl; - - ssl_certificate ${cert_pem}; - ssl_certificate_key ${cert_key}; - ssl_protocols TLSv1.1 TLSv1.2; - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:@STRENGTH'; - ssl_prefer_server_ciphers on; - ssl_dhparam ${dhparams_pem}; - ssl_session_timeout ${session_timeout}m; - - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - - location / { - proxy_pass http://127.0.0.1:${cherrypy_port}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_redirect http://127.0.0.1:${cherrypy_port}/ https://$host:${proxy_ssl_port}/; - } - - location /websockify { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } + location /websockify { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; } - ${http_config} } + +${http_config} diff --git a/src/wok/config.py.in b/src/wok/config.py.in index 1ca6f73..d0bca9b 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -76,9 +76,10 @@ class Paths(object): self.prefix = self.get_prefix() self.installed = (self.prefix == '@pkgdatadir@') self.ui_dir = self.add_prefix('ui') + self.sys_nginx_conf_dir = '@sysconfdir@/nginx/conf.d'
if self.installed: - self.nginx_conf_dir = '@sysconfdir@/nginx/conf.d' + self.nginx_conf_dir = self.sys_nginx_conf_dir self.state_dir = '@localstatedir@/lib/wok' self.log_dir = '@localstatedir@/log/wok' self.conf_dir = '@sysconfdir@/wok' @@ -119,6 +120,9 @@ class Paths(object): def get_template_path(self, resource): return os.path.join(self.ui_dir, 'pages/%s.tmpl' % resource)
+ def is_wok_installed(self): + return self.installed +
In other part of the code, we are accessing self.installed directly! Any special reason to create this new function? Seems like too Java huh
Really? I'm going to check better, but I guess the only code I saw was inside the same class or method.
paths = Paths()
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index d6cb17c..33107ee 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -55,6 +55,8 @@ messages = { "WOKUTILS0004E": _("Invalid data value '%(value)s'"), "WOKUTILS0005E": _("Invalid data unit '%(unit)s'"),
+ "WOKPROXY0001E": _("Unable to (re)start system's nginx.service. Details: '%(error)s'"), + # These messages (ending with L) are for user log purposes "WOKCOL0001L": _("Request made on collection"), "WOKRES0001L": _("Request made on resource"), diff --git a/src/wok/proxy.py b/src/wok/proxy.py index a74e88a..cf978ad 100644 --- a/src/wok/proxy.py +++ b/src/wok/proxy.py @@ -26,11 +26,12 @@
import os import pwd -import subprocess from string import Template
from wok import sslcert from wok.config import paths +from wok.exception import OperationFailed +from wok.utils import run_command
HTTP_CONFIG = """ @@ -110,18 +111,21 @@ def _create_proxy_config(options): config_file.write(data) config_file.close()
+ # If not running from the installed path (from a cloned and builded source + # code), create a symbolic link in system's dir to prevent errors on read + # SSL certifications. + if not paths.is_wok_installed(): + dst = os.path.join(paths.sys_nginx_conf_dir, "wok.conf") + if os.path.isfile(dst) or os.path.islink(dst): + os.remove(dst) + os.symlink(os.path.join(nginx_config_dir, "wok.conf"), dst) +
def start_proxy(options): """Start nginx reverse proxy.""" _create_proxy_config(options) - nginx_config_dir = paths.nginx_conf_dir - config_file = "%s/wok.conf" % nginx_config_dir - cmd = ['nginx', '-c', config_file] - subprocess.call(cmd) - - -def terminate_proxy(): - """Stop nginx process.""" - config_file = "%s/wok.conf" % paths.nginx_conf_dir - term_proxy_cmd = ['nginx', '-s', 'stop', '-c', config_file] - subprocess.call(term_proxy_cmd) + # Restart system's nginx service to reload wok configuration + cmd = ['systemctl', 'restart', 'nginx.service'] + output, error, retcode = run_command(cmd, silent=True) + if retcode != 0: + raise OperationFailed('WOKPROXY0001E', {'error': error}) diff --git a/src/wok/server.py b/src/wok/server.py index 8a02596..b1185e3 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -33,7 +33,7 @@ from wok.config import config as configParser from wok.config import paths, PluginConfig, WokConfig from wok.control import sub_nodes from wok.model import model -from wok.proxy import start_proxy, terminate_proxy +from wok.proxy import start_proxy from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler @@ -180,10 +180,6 @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), config=self.configObj) self._load_plugins(options) - - # Terminate proxy when cherrypy server is terminated - cherrypy.engine.subscribe('exit', terminate_proxy) - cherrypy.lib.sessions.init()
def _load_plugins(self, options):
-- Paulo Ricardo Paz Vital Linux Technology Center, IBM Systems http://www.ibm.com/linux/ltc/
participants (4)
-
Aline Manera
-
Daniel Henrique Barboza
-
Paulo Ricardo Paz Vital
-
pvital@linux.vnet.ibm.com