NB: Do not edit the following. It is sync'd on a nightly basis ..."
+
+; First Name
+vimbadmin_plugins.AdditionalInfo.elements.id.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.id.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.id.options.label = "LDAP Id"
+
+; First Name
+vimbadmin_plugins.AdditionalInfo.elements.first_name.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.first_name.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.first_name.options.label = "First Name"
+
+; Last Name
+vimbadmin_plugins.AdditionalInfo.elements.second_name.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.second_name.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.second_name.options.label = "Last Name"
+
+; Grade
+vimbadmin_plugins.AdditionalInfo.elements.grade.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.grade.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.grade.options.label = "Grade"
+
+; Grade Id
+vimbadmin_plugins.AdditionalInfo.elements.grade_id.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.grade_id.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.grade_id.options.label = "Grade Id"
+vimbadmin_plugins.AdditionalInfo.elements.grade_id.options.validators.digits[] = 'Digits'
+vimbadmin_plugins.AdditionalInfo.elements.grade_id.options.validators.digits[] = true
+
+; Department
+vimbadmin_plugins.AdditionalInfo.elements.department.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.department.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.department.options.label = "Department"
+
+; Department Id
+vimbadmin_plugins.AdditionalInfo.elements.department_id.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.department_id.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.department_id.options.label = "Department Id"
+vimbadmin_plugins.AdditionalInfo.elements.department_id.options.validators.digits[] = 'Digits'
+vimbadmin_plugins.AdditionalInfo.elements.department_id.options.validators.digits[] = true
+
+; Section
+vimbadmin_plugins.AdditionalInfo.elements.section.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.section.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.section.options.label = "Section"
+
+; Extension Number
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.label = "Extension Number"
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.digits[] = 'Digits'
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.digits[] = true
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.length[] = 'StringLength'
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.length[] = false
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.length.range[] = 4
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.validators.length.range[] = 4
+;;to disable autocomplete functionality
+vimbadmin_plugins.AdditionalInfo.elements.ext_no.options.autocomplete = 'off'
+
+; Direct Dial
+vimbadmin_plugins.AdditionalInfo.elements.d_dial.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.d_dial.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.d_dial.options.label = "Direct Dial"
+vimbadmin_plugins.AdditionalInfo.elements.d_dial.options.autocomplete = 'off'
+
+; Mobile
+vimbadmin_plugins.AdditionalInfo.elements.mobile.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.elements.mobile.options.required = false
+vimbadmin_plugins.AdditionalInfo.elements.mobile.options.label = "Mobile"
+vimbadmin_plugins.AdditionalInfo.elements.mobile.options.autocomplete = 'off'
+
+;;;;;;;
+;; Aliases additional information
+;;
+; First Name
+vimbadmin_plugins.AdditionalInfo.alias.elements.name.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.alias.elements.name.options.required = false
+vimbadmin_plugins.AdditionalInfo.alias.elements.name.options.label = "Name"
+
+; Extension Number
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.required = false
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.label = "Extension Number"
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.digits[] = 'Digits'
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.digits[] = true
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.length[] = 'StringLength'
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.length[] = false
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.length.range[] = 4
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.validators.length.range[] = 4
+vimbadmin_plugins.AdditionalInfo.alias.elements.ext_no.options.autocomplete = 'off'
+
+; Direct Dial
+vimbadmin_plugins.AdditionalInfo.alias.elements.d_dial.type = "Zend_Form_Element_Text"
+vimbadmin_plugins.AdditionalInfo.alias.elements.d_dial.options.required = false
+vimbadmin_plugins.AdditionalInfo.alias.elements.d_dial.options.label = "Direct Dial"
+vimbadmin_plugins.AdditionalInfo.alias.elements.d_dial.options.autocomplete = 'off'
+
+
+[production : user]
+
+includePaths.library = APPLICATION_PATH "/../library"
+includePaths.osslibrary = APPLICATION_PATH "/../vendor/opensolutions/oss-framework/src/"
+
+bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
+bootstrap.class = "Bootstrap"
+appnamespace = "ViMbAdmin"
+
+temporary_directory = APPLICATION_PATH "/../var/tmp"
+
+pluginPaths.OSS_Resource = APPLICATION_PATH "/../vendor/opensolutions/oss-framework/src/OSS/Resource"
+pluginPaths.ViMbAdmin_Resource = APPLICATION_PATH "/../library/ViMbAdmin/Resource"
+
+mini_js = 1
+mini_css = 1
+
+alias_autocomplete_min_length = 2
+
+
+
+resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
+resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"
+resources.modules[] =
+
+
+; doctrine2
+resources.doctrine2.models_path = APPLICATION_PATH
+resources.doctrine2.proxies_path = APPLICATION_PATH "/Proxies"
+resources.doctrine2.repositories_path = APPLICATION_PATH
+resources.doctrine2.xml_schema_path = APPLICATION_PATH "/../doctrine2/xml"
+resources.doctrine2.autogen_proxies = 0
+resources.doctrine2.logger = 1
+resources.doctrine2.models_namespace = "Entities"
+resources.doctrine2.proxies_namespace = "Proxies"
+resources.doctrine2.repositories_namespace = "Repositories"
+
+
+resources.doctrine2cache.autoload_method = "composer"
+;resources.doctrine2cache.type = 'ArrayCache'
+;resources.doctrine2cache.type = 'MemcacheCache'
+;resources.doctrine2cache.memcache.servers.0.host = '127.0.0.1'
+;resources.doctrine2cache.memcache.servers.0.port = '11211'
+;resources.doctrine2cache.memcache.servers.0.persistent = false
+;resources.doctrine2cache.memcache.servers.0.weight = 1
+;resources.doctrine2cache.memcache.servers.0.timeout = 1
+;resources.doctrine2cache.memcache.servers.0.retry_int = 15
+
+; resources.doctrine2cache.memcache.servers.1.host = 'xxx'
+; resources.doctrine2cache.memcache.servers.2.host = 'yyy'
+
+resources.namespace.checkip = 0
+
+resources.auth.enabled = 1
+resources.auth.oss.adapter = "OSS_Auth_Doctrine2Adapter"
+resources.auth.oss.pwhash = "bcrypt"
+resources.auth.oss.hash_cost = 9
+resources.auth.oss.entity = "\\Entities\\Admin"
+resources.auth.oss.disabled.lost-username = 1
+resources.auth.oss.disabled.lost-password = 0
+
+resources.auth.oss.rememberme.enabled = 1
+resources.auth.oss.rememberme.timeout = 2592000
+resources.auth.oss.rememberme.secure = true
+
+resources.auth.oss.lost_password.use_captcha = true
+
+resources.session.save_path = APPLICATION_PATH "/../var/session"
+resources.session.use_only_cookies = true
+resources.session.remember_me_seconds = 3600
+resources.session.name = 'VIMBADMIN3'
+
+ondemand_resources.logger.writers.stream.path = APPLICATION_PATH "/../var/log"
+ondemand_resources.logger.writers.stream.owner = {{php_user}}
+ondemand_resources.logger.writers.stream.group = {{php_user}}
+ondemand_resources.logger.writers.stream.mode = single
+ondemand_resources.logger.writers.stream.logname = vimbadmin.log
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Smarty View
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+resources.smarty.enabled = 1
+resources.smarty.templates = APPLICATION_PATH "/views"
+; resources.smarty.skin = "myskin"
+resources.smarty.compiled = APPLICATION_PATH "/../var/templates_c"
+resources.smarty.cache = APPLICATION_PATH "/../var/cache"
+resources.smarty.config = APPLICATION_PATH "/configs/smarty"
+resources.smarty.plugins[] = APPLICATION_PATH "/../library/ViMbAdmin/Smarty/functions"
+resources.smarty.plugins[] = APPLICATION_PATH "/../vendor/opensolutions/oss-framework/src/OSS/Smarty/functions"
+resources.smarty.plugins[] = APPLICATION_PATH "/../vendor/smarty/smarty/libs/plugins"
+resources.smarty.plugins[] = APPLICATION_PATH "/../vendor/smarty/smarty/libs/sysplugins"
+resources.smarty.debugging = 0
+
+
+
+
+[development : production]
+
+mini_js = 0
+mini_css = 0
+
+phpSettings.display_startup_errors = 1
+phpSettings.display_errors = 1
+resources.frontController.params.displayExceptions = 1
diff --git a/ee/cli/templates/virtual_alias_maps.mustache b/ee/cli/templates/virtual_alias_maps.mustache
new file mode 100644
index 00000000..b7919c74
--- /dev/null
+++ b/ee/cli/templates/virtual_alias_maps.mustache
@@ -0,0 +1,5 @@
+user = vimbadmin
+password = {{password}}
+hosts = {{host}}
+dbname = vimbadmin
+query = SELECT goto FROM alias WHERE address = '%s' AND active = '1'
diff --git a/ee/cli/templates/virtual_domains_maps.mustache b/ee/cli/templates/virtual_domains_maps.mustache
new file mode 100644
index 00000000..2dce79a0
--- /dev/null
+++ b/ee/cli/templates/virtual_domains_maps.mustache
@@ -0,0 +1,5 @@
+user = vimbadmin
+password = {{password}}
+hosts = {{host}}
+dbname = vimbadmin
+query = SELECT domain FROM domain WHERE domain = '%s' AND backupmx = '0' AND active = '1'
diff --git a/ee/cli/templates/virtual_mailbox_maps.mustache b/ee/cli/templates/virtual_mailbox_maps.mustache
new file mode 100644
index 00000000..f8aafdaf
--- /dev/null
+++ b/ee/cli/templates/virtual_mailbox_maps.mustache
@@ -0,0 +1,7 @@
+user = vimbadmin
+password = {{password}}
+hosts = {{host}}
+dbname = vimbadmin
+table = mailbox
+select_field = maildir
+where_field = username
diff --git a/ee/cli/templates/virtualconf.mustache b/ee/cli/templates/virtualconf.mustache
new file mode 100644
index 00000000..cadb946b
--- /dev/null
+++ b/ee/cli/templates/virtualconf.mustache
@@ -0,0 +1,34 @@
+
+server {
+
+ {{#multisite}}
+ # Uncomment the following line for domain mapping
+ # listen 80 default_server;
+ {{/multisite}}
+
+ server_name {{^vma}}{{^rc}}{{site_name}}{{/rc}}{{/vma}} {{#vma}}vma.*{{/vma}} {{#rc}}webmail.*{{/rc}} {{^vma}}{{^rc}}{{#multisite}}*{{/multisite}}{{^multisite}}www{{/multisite}}.{{site_name}}{{/rc}}{{/vma}};
+
+ {{#multisite}}
+ # Uncomment the following line for domain mapping
+ #server_name_in_redirect off;
+ {{/multisite}}
+
+ access_log /var/log/nginx/{{site_name}}.access.log {{^static}}rt_cache{{/static}};
+ error_log /var/log/nginx/{{site_name}}.error.log;
+ {{^vma}}{{^rc}}root {{webroot}}/htdocs;{{/rc}}{{/vma}}
+ {{#vma}}root /var/www/22222/htdocs/vimbadmin/public;{{/vma}}
+ {{#rc}}root /var/www/roundcubemail/htdocs/;{{/rc}}
+
+ index {{^static}}index.php{{/static}} index.html index.htm;
+
+ {{#static}}
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+ {{/static}}
+
+ {{^static}}include{{/static}} {{#basic}}common/php.conf;{{/basic}}{{#w3tc}}common/w3tc.conf;{{/w3tc}}{{#wpfc}}common/wpfc.conf;{{/wpfc}} {{#wpsc}}common/wpsc.conf;{{/wpsc}}
+ {{#wpsubdir}}include common/wpsubdir.conf;{{/wpsubdir}}
+ {{#wp}}include common/wpcommon.conf;{{/wp}}
+ include common/locations.conf;
+}
diff --git a/ee/cli/templates/w3tc.mustache b/ee/cli/templates/w3tc.mustache
new file mode 100644
index 00000000..5b162822
--- /dev/null
+++ b/ee/cli/templates/w3tc.mustache
@@ -0,0 +1,31 @@
+
+# W3TC NGINX CONFIGURATION
+# DO NOT MODIFY, ALL CHNAGES LOST AFTER UPDATE EasyEngine (ee)
+set $cache_uri $request_uri;
+# POST requests and URL with a query string should always go to php
+if ($request_method = POST) {
+ set $cache_uri 'null cache';
+}
+if ($query_string != "") {
+ set $cache_uri 'null cache';
+}
+# Don't cache URL containing the following segments
+if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
+ set $cache_uri 'null cache';
+}
+# Don't use the cache for logged in users or recent commenter
+if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
+ set $cache_uri 'null cache';
+}
+# Use cached or actual file if they exists, Otherwise pass request to WordPress
+location / {
+ try_files /wp-content/cache/page_enhanced/${host}${cache_uri}_index.html $uri $uri/ /index.php?$args;
+}
+location ~ ^/wp-content/cache/minify/(.+\.(css|js))$ {
+ try_files $uri /wp-content/plugins/w3-total-cache/pub/minify.php?file=$1;
+}
+location ~ \.php$ {
+ try_files $uri =404;
+ include fastcgi_params;
+ fastcgi_pass php;
+}
diff --git a/ee/cli/templates/wpcommon.mustache b/ee/cli/templates/wpcommon.mustache
new file mode 100644
index 00000000..e6b34782
--- /dev/null
+++ b/ee/cli/templates/wpcommon.mustache
@@ -0,0 +1,35 @@
+# WordPress COMMON SETTINGS
+# DO NOT MODIFY, ALL CHNAGES LOST AFTER UPDATE EasyEngine (ee)
+# Limit access to avoid brute force attack
+location = /wp-login.php {
+ limit_req zone=one burst=1 nodelay;
+ include fastcgi_params;
+ fastcgi_pass php;
+}
+# Disable wp-config.txt
+location = /wp-config.txt {
+ deny all;
+ access_log off;
+ log_not_found off;
+}
+# Disallow php in upload folder
+location /wp-content/uploads/ {
+ location ~ \.php$ {
+ #Prevent Direct Access Of PHP Files From Web Browsers
+ deny all;
+ }
+}
+# Yoast sitemap
+location ~ ([^/]*)sitemap(.*)\.x(m|s)l$ {
+ rewrite ^/sitemap\.xml$ /sitemap_index.xml permanent;
+ rewrite ^/([a-z]+)?-?sitemap\.xsl$ /index.php?xsl=$1 last;
+ # Rules for yoast sitemap with wp|wpsubdir|wpsubdomain
+ rewrite ^.*/sitemap_index\.xml$ /index.php?sitemap=1 last;
+ rewrite ^.*/([^/]+?)-sitemap([0-9]+)?\.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;
+ # Following lines are options. Needed for WordPress seo addons
+ rewrite ^/news_sitemap\.xml$ /index.php?sitemap=wpseo_news last;
+ rewrite ^/locations\.kml$ /index.php?sitemap=wpseo_local_kml last;
+ rewrite ^/geo_sitemap\.xml$ /index.php?sitemap=wpseo_local last;
+ rewrite ^/video-sitemap\.xsl$ /index.php?xsl=video last;
+ access_log off;
+}
diff --git a/ee/cli/templates/wpfc.mustache b/ee/cli/templates/wpfc.mustache
new file mode 100644
index 00000000..ff5240c7
--- /dev/null
+++ b/ee/cli/templates/wpfc.mustache
@@ -0,0 +1,36 @@
+# WPFC NGINX CONFIGURATION
+# DO NOT MODIFY, ALL CHNAGES LOST AFTER UPDATE EasyEngine (ee)
+set $skip_cache 0;
+# POST requests and URL with a query string should always go to php
+if ($request_method = POST) {
+ set $skip_cache 1;
+}
+if ($query_string != "") {
+ set $skip_cache 1;
+}
+# Don't cache URL containing the following segments
+if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
+ set $skip_cache 1;
+}
+# Don't use the cache for logged in users or recent commenter
+if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
+ set $skip_cache 1;
+}
+# Use cached or actual file if they exists, Otherwise pass request to WordPress
+location / {
+ try_files $uri $uri/ /index.php?$args;
+}
+location ~ ^/wp-content/cache/minify/(.+\.(css|js))$ {
+ try_files $uri /wp-content/plugins/w3-total-cache/pub/minify.php?file=$1;
+}
+location ~ \.php$ {
+ try_files $uri =404;
+ include fastcgi_params;
+ fastcgi_pass php;
+ fastcgi_cache_bypass $skip_cache;
+ fastcgi_no_cache $skip_cache;
+ fastcgi_cache WORDPRESS;
+}
+location ~ /purge(/.*) {
+ fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
+}
diff --git a/ee/cli/templates/wpsc.mustache b/ee/cli/templates/wpsc.mustache
new file mode 100644
index 00000000..2600a795
--- /dev/null
+++ b/ee/cli/templates/wpsc.mustache
@@ -0,0 +1,31 @@
+# WPSC NGINX CONFIGURATION
+# DO NOT MODIFY, ALL CHNAGES LOST AFTER UPDATE EasyEngine (ee)
+set $cache_uri $request_uri;
+# POST requests and URL with a query string should always go to php
+if ($request_method = POST) {
+ set $cache_uri 'null cache';
+}
+if ($query_string != "") {
+ set $cache_uri 'null cache';
+}
+# Don't cache URL containing the following segments
+if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
+ set $cache_uri 'null cache';
+}
+# Don't use the cache for logged in users or recent commenter
+if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
+ set $cache_uri 'null cache';
+}
+# Use cached or actual file if they exists, Otherwise pass request to WordPress
+location / {
+ # If we add index.php?$args its break WooCommerce like plugins
+ # Ref: #330
+ try_files /wp-content/cache/supercache/$http_host/$cache_uri/index.html $uri $uri/ /index.php;
+}
+location ~ \.php$ {
+ try_files $uri =404;
+ include fastcgi_params;
+ fastcgi_pass php;
+ # Following line is needed by WP Super Cache plugin
+ fastcgi_param SERVER_NAME $http_host;
+}
diff --git a/ee/cli/templates/wpsubdir.mustache b/ee/cli/templates/wpsubdir.mustache
new file mode 100644
index 00000000..7b65841f
--- /dev/null
+++ b/ee/cli/templates/wpsubdir.mustache
@@ -0,0 +1,10 @@
+# WPSUBDIRECTORY NGINX CONFIGURATION
+# DO NOT MODIFY, ALL CHNAGES LOST AFTER UPDATE EasyEngine (ee)
+if (!-e $request_filename) {
+ # Redirect wp-admin to wp-admin/
+ rewrite /wp-admin$ $scheme://$host$uri/ permanent;
+ # Redirect wp-* files/folders
+ rewrite ^(/[^/]+)?(/wp-.*) $2 last;
+ # Redirect other php files
+ rewrite ^(/[^/]+)?(/.*\.php) $2 last;
+}
diff --git a/ee/core/__init__.py b/ee/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ee/core/addswap.py b/ee/core/addswap.py
new file mode 100644
index 00000000..717d67a1
--- /dev/null
+++ b/ee/core/addswap.py
@@ -0,0 +1,27 @@
+"""EasyEngine SWAP creation"""
+from ee.core.variables import EEVariables
+from ee.core.shellexec import EEShellExec
+from ee.core.fileutils import EEFileUtils
+from ee.core.logging import Log
+
+
+class EESwap():
+ """Manage Swap"""
+
+ def __init__():
+ """Initialize """
+ pass
+
+ def add(self):
+ """Swap addition with EasyEngine"""
+ if EEVariables.ee_ram < 512:
+ if EEVariables.ee_swap < 1000:
+ Log.info(self, "Adding SWAP")
+ EEShellExec.cmd_exec(self, "dd if=/dev/zero of=/ee-swapfile "
+ "bs=1024 count=1048k")
+ EEShellExec.cmd_exec(self, "mkswap /ee-swapfile")
+ EEFileUtils.chown(self, "/ee-swapfile", "root", "root")
+ EEFileUtils.chmod(self, "/ee-swapfile", 0o600)
+ EEShellExec.cmd_exec(self, "swapon /ee-swapfile")
+ with open("/etc/fstab", "a") as swap_file:
+ swap_file.write("/ee-swapfile\tnone\tswap\tsw\t0 0")
diff --git a/ee/core/apt_repo.py b/ee/core/apt_repo.py
new file mode 100644
index 00000000..d3e82ed9
--- /dev/null
+++ b/ee/core/apt_repo.py
@@ -0,0 +1,78 @@
+"""EasyEngine packages repository operations"""
+from ee.core.shellexec import EEShellExec
+from ee.core.variables import EEVariables
+import os
+
+
+class EERepo():
+ """Manage Repositories"""
+
+ def __init__(self):
+ """Initialize """
+ pass
+
+ def add(self, repo_url=None, ppa=None):
+ """
+ This function used to add apt repositories and or ppa's
+ If repo_url is provided adds repo file to
+ /etc/apt/sources.list.d/
+ If ppa is provided add apt-repository using
+ add-apt-repository
+ command.
+ """
+
+ if repo_url is not None:
+ repo_file_path = ("/etc/apt/sources.list.d/"
+ + EEVariables().ee_repo_file)
+ try:
+ if not os.path.isfile(repo_file_path):
+ with open(repo_file_path, "a") as repofile:
+ repofile.write(repo_url)
+ repofile.write('\n')
+ repofile.close()
+ elif repo_url not in open(repo_file_path).read():
+ with open(repo_file_path, "a") as repofile:
+ repofile.write(repo_url)
+ repofile.write('\n')
+ repofile.close()
+ return True
+ except IOError as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "File I/O error.")
+ except Exception as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to add repo")
+ if ppa is not None:
+ if EEVariables.ee_platform_distro == 'squeeze':
+ print("Cannot add repo for {distro}"
+ .format(distro=EEVariables.ee_platform_distro))
+ else:
+ EEShellExec.cmd_exec(self, "add-apt-repository -y "
+ "'{ppa_name}'"
+ .format(ppa_name=ppa))
+
+ def remove(self, ppa=None):
+ """
+ This function used to remove ppa's
+ If ppa is provided adds repo file to
+ /etc/apt/sources.list.d/
+ command.
+ """
+ EEShellExec.cmd_exec(self, "add-apt-repository -y "
+ "--remove '{ppa_name}'"
+ .format(ppa_name=repo_url))
+
+ def add_key(self, keyids, keyserver=None):
+ """
+ This function adds imports repository keys from keyserver.
+ default keyserver is hkp://keys.gnupg.net
+ user can provide other keyserver with keyserver="hkp://xyz"
+ """
+ if keyserver is None:
+ EEShellExec.cmd_exec(self, "gpg --keyserver {serv}"
+ .format(serv=(keyserver or
+ "hkp://keys.gnupg.net"))
+ + " --recv-keys {key}".format(key=keyids))
+ EEShellExec.cmd_exec(self, "gpg -a --export --armor {0}"
+ .format(keyids)
+ + " | apt-key add - ")
diff --git a/ee/core/aptget.py b/ee/core/aptget.py
new file mode 100644
index 00000000..b33b3d1d
--- /dev/null
+++ b/ee/core/aptget.py
@@ -0,0 +1,198 @@
+"""EasyEngine package installation using apt-get module."""
+import apt
+import apt_pkg
+import sys
+from ee.core.logging import Log
+from sh import apt_get
+
+
+class EEAptGet():
+ """Generic apt-get intialisation"""
+
+ def update(self):
+ """
+ Similar to `apt-get upgrade`
+ """
+ try:
+ apt_cache = apt.cache.Cache()
+ import sys
+ orig_out = sys.stdout
+ sys.stdout = open(self.app.config.get('log.logging', 'file'), 'a')
+ apt_cache.update(apt.progress.text.AcquireProgress())
+ sys.stdout = orig_out
+ # success = (apt_cache.commit(
+ # apt.progress.text.AcquireProgress(),
+ # apt.progress.base.InstallProgress()))
+ # #apt_cache.close()
+ # return success
+ except AttributeError as e:
+ Log.error(self, 'AttributeError: ' + str(e))
+ except Exception as e:
+ Log.debug(self, 'SystemError: ' + str(e))
+ Log.error(self, 'Unable to Fetch update')
+
+ def dist_upgrade():
+ """
+ Similar to `apt-get upgrade`
+ """
+ try:
+ apt_cache = apt.cache.Cache()
+ apt_cache.update()
+ apt_cache.open(None)
+ apt_cache.upgrade(True)
+ success = (apt_cache.commit(
+ apt.progress.text.AcquireProgress(),
+ apt.progress.base.InstallProgress()))
+ #apt_cache.close()
+ return success
+ except AttributeError as e:
+ Log.error(self, 'AttributeError: ' + str(e))
+ except FetchFailedException as e:
+ Log.debug(self, 'SystemError: ' + str(e))
+ Log.error(self, 'Unable to Fetch update')
+
+ def install(self, packages):
+ """
+ Similar to `apt-get install`
+ """
+ apt_pkg.init()
+ # #apt_pkg.PkgSystemLock()
+ global apt_cache
+ apt_cache = apt.cache.Cache()
+
+ def install_package(self, package_name):
+ pkg = apt_cache[package_name.strip()]
+ if package_name.strip() in apt_cache:
+ if pkg.is_installed:
+ #apt_pkg.PkgSystemUnLock()
+ Log.debug(self, 'Trying to install a package that '
+ 'is already installed (' +
+ package_name.strip() + ')')
+ #apt_cache.close()
+ return False
+ else:
+ try:
+ # print(pkg.name)
+ pkg.mark_install()
+ except Exception as e:
+ Log.debug(self, str(e))
+ Log.error(self, str(e))
+ else:
+ #apt_cache.close()
+ Log.error(self, 'Unknown package selected (' +
+ package_name.strip() + ')')
+
+ for package in packages:
+ if not install_package(self, package):
+ continue
+
+ if apt_cache.install_count > 0:
+ try:
+ #apt_pkg.PkgSystemUnLock()
+ orig_out = sys.stdout
+ sys.stdout = open(self.app.config.get('log.logging', 'file'),
+ 'a')
+ result = apt_cache.commit(apt.progress.text.AcquireProgress(),
+ apt.progress.base.InstallProgress())
+ sys.stdout = orig_out
+ #apt_cache.close()
+ return result
+ except SystemError as e:
+ Log.debug(self, 'SystemError: ' + str(e))
+ Log.error(self, 'SystemError: ' + str(e))
+ #apt_cache.close()
+ except Exception as e:
+ Log.debug(self, str(e))
+ Log.error(self, str(e))
+
+ def remove(self, packages, auto=False, purge=False):
+ """
+ Similar to `apt-get remove/purge`
+ purge packages if purge=True
+ """
+ apt_pkg.init()
+ # apt_pkg.PkgSystemLock()
+ global apt_cache
+ apt_cache = apt.cache.Cache()
+
+ def remove_package(self, package_name, purge=False):
+ pkg = apt_cache[package_name.strip()]
+ if package_name.strip() in apt_cache:
+ if not pkg.is_installed:
+ # apt_pkg.PkgSystemUnLock()
+ Log.debug(self, 'Trying to uninstall a package '
+ 'that is not installed (' +
+ package_name.strip() + ')')
+ return False
+ else:
+ try:
+ # print(pkg.name)
+ pkg.mark_delete(purge)
+ except SystemError as e:
+ Log.debug(self, 'SystemError: ' + str(e))
+ return False
+ else:
+ # apt_cache.close()
+ Log.error(self, 'Unknown package selected (' +
+ package_name.strip() + ')')
+
+ for package in packages:
+ if not remove_package(self, package, purge=purge):
+ continue
+
+ if apt_cache.delete_count > 0:
+ try:
+ # apt_pkg.PkgSystemUnLock()
+ orig_out = sys.stdout
+ sys.stdout = open(self.app.config.get('log.logging', 'file'),
+ 'a')
+ result = apt_cache.commit(apt.progress.text.AcquireProgress(),
+ apt.progress.base.InstallProgress())
+ sys.stdout = orig_out
+ # apt_cache.close()
+ return result
+ except SystemError as e:
+ Log.debug(self, 'SystemError: ' + str(e))
+ return False
+ except Exception as e:
+ Log.debug(self, str(e))
+ Log.error(self, str(e))
+ # apt_cache.close()
+
+ def auto_clean(self):
+ """
+ Similar to `apt-get autoclean`
+ """
+ try:
+ orig_out = sys.stdout
+ sys.stdout = open(self.app.config.get('log.logging', 'file'), 'a')
+ apt_get.autoclean("-y")
+ sys.stdout = orig_out
+ except ErrorReturnCode as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to apt-get autoclean")
+
+ def auto_remove(self):
+ """
+ Similar to `apt-get autoremove`
+ """
+ try:
+ Log.debug(self, "Running apt-get autoremove")
+ apt_get.autoremove("-y")
+ except ErrorReturnCode as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to apt-get autoremove")
+
+ def is_installed(self, package_name):
+ """
+ Checks if package is available in cache and is installed or not
+ returns True if installed otherwise returns False
+ """
+ apt_cache = apt.cache.Cache()
+ apt_cache.open()
+ if (package_name.strip() in apt_cache and
+ apt_cache[package_name.strip()].is_installed):
+ # apt_cache.close()
+ return True
+ # apt_cache.close()
+ return False
diff --git a/ee/core/checkfqdn.py b/ee/core/checkfqdn.py
new file mode 100644
index 00000000..915b2e24
--- /dev/null
+++ b/ee/core/checkfqdn.py
@@ -0,0 +1,23 @@
+from ee.core.shellexec import EEShellExec
+from ee.core.variables import EEVariables
+import os
+
+
+def check_fqdn(self, ee_host):
+ """FQDN check with EasyEngine, for mail server hostname must be FQDN"""
+ #ee_host=os.popen("hostname -f | tr -d '\n'").read()
+ if '.' in ee_host:
+ EEVariables.ee_fqdn = ee_host
+ with open('/etc/hostname', 'w') as hostfile:
+ hostfile.write(ee_host)
+
+ EEShellExec.cmd_exec(self, "sed -i \"1i\\127.0.0.1 {0}\" /etc/hosts"
+ .format(ee_host))
+ if EEVariables.ee_platform_distro == 'debian':
+ EEShellExec.cmd_exec(self, "/etc/init.d/hostname.sh start")
+ else:
+ EEShellExec.cmd_exec(self, "service hostname restart")
+
+ else:
+ ee_host = input("Enter hostname [fqdn]:")
+ check_fqdn(self, ee_host)
diff --git a/ee/core/database.py b/ee/core/database.py
new file mode 100644
index 00000000..573ee099
--- /dev/null
+++ b/ee/core/database.py
@@ -0,0 +1,24 @@
+"""EasyEngine generic database creation module"""
+from sqlalchemy import create_engine
+from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.ext.declarative import declarative_base
+from ee.core.variables import EEVariables
+
+# db_path = self.app.config.get('site', 'db_path')
+engine = create_engine(EEVariables.ee_db_uri, convert_unicode=True)
+db_session = scoped_session(sessionmaker(autocommit=False,
+ autoflush=False,
+ bind=engine))
+Base = declarative_base()
+Base.query = db_session.query_property()
+
+
+def init_db():
+ """
+ Initializes and creates all tables from models into the database
+ """
+ # import all modules here that might define models so that
+ # they will be registered properly on the metadata. Otherwise
+ # you will have to import them first before calling init_db()
+ import ee.core.models
+ Base.metadata.create_all(bind=engine)
diff --git a/ee/core/domainvalidate.py b/ee/core/domainvalidate.py
new file mode 100644
index 00000000..a9400c9a
--- /dev/null
+++ b/ee/core/domainvalidate.py
@@ -0,0 +1,24 @@
+"""EasyEngine domain validation module."""
+from urllib.parse import urlparse
+
+
+def ValidateDomain(url):
+ """
+ This function returns domain name removing http:// and https://
+ returns domain name only with or without www as user provided.
+ """
+
+ # Check if http:// or https:// present remove it if present
+ domain_name = url.split('/')
+ if 'http:' in domain_name or 'https:' in domain_name:
+ domain_name = domain_name[2]
+ else:
+ domain_name = domain_name[0]
+ www_domain_name = domain_name.split('.')
+ final_domain = ''
+ if www_domain_name[0] == 'www':
+ final_domain = '.'.join(www_domain_name[1:])
+ return final_domain
+ else:
+ final_domain = domain_name
+ return (final_domain, domain_name)
diff --git a/ee/core/download.py b/ee/core/download.py
new file mode 100644
index 00000000..c0796658
--- /dev/null
+++ b/ee/core/download.py
@@ -0,0 +1,43 @@
+"""EasyEngine download core classes."""
+import urllib.request
+import urllib.error
+import os
+from ee.core.logging import Log
+
+
+class EEDownload():
+ """Method to download using urllib"""
+ def __init__():
+ pass
+
+ def download(self, packages):
+ """Download packages, packges must be list in format of
+ [url, path, package name]"""
+ for package in packages:
+ url = package[0]
+ filename = package[1]
+ pkg_name = package[2]
+ try:
+ directory = os.path.dirname(filename)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ Log.info(self, "Downloading {0:20}".format(pkg_name), end=' ')
+ urllib.request.urlretrieve(url, filename)
+ Log.info(self, "{0}".format("[" + Log.ENDC + "Done"
+ + Log.OKBLUE + "]"))
+ except urllib.error.URLError as e:
+ Log.debug(self, "[{err}]".format(err=str(e.reason)))
+ Log.error(self, "Unable to donwload file, {0}"
+ .format(filename))
+ return False
+ except urllib.error.HTTPError as e:
+ Log.error(self, "Package download failed. {0}"
+ .format(pkg_name))
+ Log.debug(self, "[{err}]".format(err=str(e.reason)))
+ return False
+ except urllib.error.ContentTooShortError as e:
+ Log.debug(self, "{0}{1}".format(e.errno, e.strerror))
+ Log.error(self, "Package download failed. The amount of the"
+ " downloaded data is less than "
+ "the expected amount \{0} ".format(pkg_name))
+ return False
diff --git a/ee/core/exc.py b/ee/core/exc.py
new file mode 100644
index 00000000..06579f41
--- /dev/null
+++ b/ee/core/exc.py
@@ -0,0 +1,26 @@
+"""EasyEngine exception classes."""
+
+
+class EEError(Exception):
+ """Generic errors."""
+ def __init__(self, msg):
+ Exception.__init__(self)
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+
+class EEConfigError(EEError):
+ """Config related errors."""
+ pass
+
+
+class EERuntimeError(EEError):
+ """Generic runtime errors."""
+ pass
+
+
+class EEArgumentError(EEError):
+ """Argument related errors."""
+ pass
diff --git a/ee/core/extract.py b/ee/core/extract.py
new file mode 100644
index 00000000..d8db19c5
--- /dev/null
+++ b/ee/core/extract.py
@@ -0,0 +1,21 @@
+"""EasyEngine extarct core classes."""
+import tarfile
+import os
+from ee.core.logging import Log
+
+
+class EEExtract():
+ """Method to extract from tar.gz file"""
+
+ def extract(self, file, path):
+ """Function to extract tar.gz file"""
+ try:
+ tar = tarfile.open(file)
+ tar.extractall(path=path)
+ tar.close()
+ os.remove(file)
+ return True
+ except tarfile.TarError as e:
+ Log.debug(self, "{0}{1}".format(e.errno, e.strerror))
+ Log.error(self, 'Unable to extract file \{0}'.format(file))
+ return False
diff --git a/ee/core/fileutils.py b/ee/core/fileutils.py
new file mode 100644
index 00000000..5e8a4b68
--- /dev/null
+++ b/ee/core/fileutils.py
@@ -0,0 +1,224 @@
+"""EasyEngine file utils core classes."""
+import shutil
+import os
+import sys
+import glob
+import shutil
+import pwd
+import fileinput
+from ee.core.logging import Log
+
+
+class EEFileUtils():
+ """Utilities to operate on files"""
+ def __init__():
+ pass
+
+ def remove(self, filelist):
+ """remove files from given path"""
+ for file in filelist:
+ if os.path.isfile(file):
+ Log.info(self, "Removing {0:65}".format(file), end=' ')
+ os.remove(file)
+ Log.info(self, "{0}".format("[" + Log.ENDC + "Done" +
+ Log.OKBLUE + "]"))
+ Log.debug(self, 'file Removed')
+ if os.path.isdir(file):
+ try:
+ Log.info(self, "Removing {0:65}".format(file), end=' ')
+ shutil.rmtree(file)
+ Log.info(self, "{0}".format("[" + Log.ENDC + "Done" +
+ Log.OKBLUE + "]"))
+ except shutil.Error as e:
+ Log.debug(self, "{err}".format(err=str(e.reason)))
+ Log.error(self, 'Unable to Remove file ')
+
+ def create_symlink(self, paths, errormsg=''):
+ """
+ Create symbolic links provided in list with first as source
+ and second as destination
+ """
+ src = paths[0]
+ dst = paths[1]
+ if not os.path.islink(dst):
+ try:
+ os.symlink(src, dst)
+ except Exception as e:
+ Log.debug(self, "{0}{1}".format(e.errno, e.strerror))
+ Log.error(self, "Unable to create symbolic link ...\n ")
+ else:
+ Log.debug(self, "Destination: {0} exists".format(dst))
+
+ def remove_symlink(self, filepath):
+ """
+ Removes symbolic link for the path provided with filepath
+ """
+ try:
+ os.unlink(filepath)
+ except Exception as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to reomove symbolic link ...\n")
+
+ def copyfile(self, src, dest):
+ """
+ Copies files:
+ src : source path
+ dest : destination path
+ """
+ try:
+ shutil.copy2(src, dest)
+ except shutil.Error as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, 'Unable to copy file from {0} to {1}'
+ .format(src, dest))
+ except IOError as e:
+ Log.debug(self, "{e}".format(e.strerror))
+ Log.error(self, "Unable to copy file from {0} to {1}"
+ .fromat(src, dest))
+
+ def searchreplace(self, fnm, sstr, rstr):
+ """
+ Search replace strings in file
+ fnm : filename
+ sstr: search string
+ rstr: replace string
+ """
+ try:
+ for line in fileinput.input(fnm, inplace=True):
+ print(line.replace(sstr, rstr), end='')
+ fileinput.close()
+ except Exception as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to search {0} and replace {1} {2}"
+ .format(fnm, sstr, rstr))
+
+ def mvfile(self, src, dst):
+ """
+ Moves file from source path to destination path
+ src : source path
+ dst : Destination path
+ """
+ try:
+ Log.debug(self, "Moving file from {0} to {1}".format(src, dst))
+ shutil.move(src, dst)
+ except Exception as e:
+ Log.debug(self, "{err}".format(err=e))
+ Log.error(self, 'Unable to move file from {0} to {1}'
+ .format(src, dst))
+
+ def chdir(self, path):
+ """
+ Change Directory to path specified
+ Path : path for destination directory
+ """
+ try:
+ os.chdir(path)
+ except OSError as e:
+ Log.debug(self, "{err}".format(err=e.strerror))
+ Log.error(self, 'Unable to Change Directory {0}'.format(path))
+
+ def chown(self, path, user, group, recursive=False):
+ """
+ Change Owner for files
+ change owner for file with path specified
+ user: username of owner
+ group: group of owner
+ recursive: if recursive is True change owner for all
+ files in directory
+ """
+ userid = pwd.getpwnam(user)[2]
+ groupid = pwd.getpwnam(user)[3]
+ try:
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for d in dirs:
+ os.chown(os.path.join(root, d), userid,
+ groupid)
+ for f in files:
+ os.chown(os.path.join(root, f), userid,
+ groupid)
+ else:
+ os.chown(path, userid, groupid)
+ except shutil.Error as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to change owner : {0}".format(path))
+ except Exception as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to change owner : {0} ".format(path))
+
+ def chmod(self, path, perm, recursive=False):
+ """
+ Changes Permission for files
+ path : file path permission to be changed
+ perm : permissions to be given
+ recursive: change permission recursively for all files
+ """
+ try:
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for d in dirs:
+ os.chmod(os.path.join(root, d), perm)
+ for f in files:
+ os.chmod(os.path.join(root, f), perm)
+ else:
+ os.chmod(path, perm)
+ except OSError as e:
+ Log.debug(self, "{0}".format(e.strerror))
+ Log.error(self, "Unable to change owner : {0}".format(path))
+
+ def mkdir(self, path):
+ """
+ create directories.
+ path : path for directory to be created
+ Similar to `mkdir -p`
+ """
+ try:
+ os.makedirs(path)
+ except OSError as e:
+ Log.debug(self, "{0}".format(e.strerror))
+ Log.error(self, "Unable to create directory {0} ".format(path))
+
+ def isexist(self, path):
+ """
+ Check if file exist on given path
+ """
+ try:
+ if os.path.exists(path):
+ return (True)
+ else:
+ return (False)
+ except OSError as e:
+ Log.debug(self, "{0}".format(e.strerror))
+ Log.error(self, "Unable to check path {0}".format(path))
+
+ def grep(self, fnm, sstr):
+ """
+ Searches for string in file and returns the matched line.
+ """
+ try:
+ for line in open(fnm):
+ if sstr in line:
+ return line
+ except OSError as e:
+ Log.debug(self, "{0}".format(e.strerror))
+ Log.error(self, "Unable to Search string {0} in {1}"
+ .format(sstr, fnm))
+
+ def rm(self, path):
+ """
+ Remove files
+ """
+ if EEFileUtils.isexist(self, path):
+ try:
+ if os.path.isdir(path):
+ shutil.rmtree(path)
+ else:
+ os.remove(path)
+ except shutil.Error as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to remove directory : {0} "
+ .format(path))
+ except OSError as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to remove file : {0} "
+ .format(path))
diff --git a/ee/core/git.py b/ee/core/git.py
new file mode 100644
index 00000000..5d587c81
--- /dev/null
+++ b/ee/core/git.py
@@ -0,0 +1,57 @@
+"""EasyEngine GIT module"""
+from sh import git, ErrorReturnCode
+from ee.core.logging import Log
+import os
+
+
+class EEGit:
+ """Intialization of core variables"""
+ def ___init__():
+ # TODO method for core variables
+ pass
+
+ def add(self, paths, msg="Intializating"):
+ """
+ Initializes Directory as repository if not already git repo.
+ and adds uncommited changes automatically
+ """
+ for path in paths:
+ global git
+ git = git.bake("--git-dir={0}/.git".format(path),
+ "--work-tree={0}".format(path))
+ if os.path.isdir(path):
+ if not os.path.isdir(path+"/.git"):
+ try:
+ Log.debug(self, "EEGit: git init at {0}"
+ .format(path))
+ git.init(path)
+ except ErrorReturnCode as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to git init at {0}"
+ .format(path))
+ status = git.status("-s")
+ if len(status.splitlines()) > 0:
+ try:
+ Log.debug(self, "EEGit: git commit at {0}"
+ .format(path))
+ git.add("--all")
+ git.commit("-am {0}".format(msg))
+ except ErrorReturnCode as e:
+ Log.debug(self, "{0}".format(e))
+ Log.error(self, "Unable to git commit at {0} "
+ .format(path))
+ else:
+ Log.debug(self, "EEGit: Path {0} not present".format(path))
+
+ def checkfilestatus(self, repo, filepath):
+ """
+ Checks status of file, If its tracked or untracked.
+ """
+ global git
+ git = git.bake("--git-dir={0}/.git".format(repo),
+ "--work-tree={0}".format(repo))
+ status = git.status("-s", "{0}".format(filepath))
+ if len(status.splitlines()) > 0:
+ return True
+ else:
+ return False
diff --git a/ee/core/logging.py b/ee/core/logging.py
new file mode 100644
index 00000000..a02d47c6
--- /dev/null
+++ b/ee/core/logging.py
@@ -0,0 +1,43 @@
+"""EasyEngine log module"""
+
+
+class Log:
+ """
+ Logs messages with colors for different messages
+ according to functions
+ """
+ HEADER = '\033[95m'
+ OKBLUE = '\033[94m'
+ OKGREEN = '\033[92m'
+ WARNING = '\033[93m'
+ FAIL = '\033[91m'
+ ENDC = '\033[0m'
+ BOLD = '\033[1m'
+ UNDERLINE = '\033[4m'
+
+ def error(self, msg):
+ """
+ Logs error into log file
+ """
+ print(Log.FAIL + msg + Log.ENDC)
+ self.app.log.error(Log.FAIL + msg + Log.ENDC)
+ self.app.close(1)
+
+ def info(self, msg, end='\n'):
+ """
+ Logs info messages into log file
+ """
+ print(Log.OKBLUE + msg + Log.ENDC, end=end)
+ self.app.log.info(Log.OKBLUE + msg + Log.ENDC)
+
+ def warn(self, msg):
+ """
+ Logs warning into log file
+ """
+ self.app.log.warn(Log.BOLD + msg + Log.ENDC)
+
+ def debug(self, msg):
+ """
+ Logs debug messages into log file
+ """
+ self.app.log.debug(Log.HEADER + msg + Log.ENDC)
diff --git a/ee/core/logwatch.py b/ee/core/logwatch.py
new file mode 100644
index 00000000..818f50d3
--- /dev/null
+++ b/ee/core/logwatch.py
@@ -0,0 +1,195 @@
+
+"""
+Real time log files watcher supporting log rotation.
+"""
+
+import os
+import time
+import errno
+import stat
+from ee.core.logging import Log
+
+
+class LogWatcher(object):
+ """Looks for changes in all files of a directory.
+ This is useful for watching log file changes in real-time.
+ It also supports files rotation.
+
+ Example:
+
+ >>> def callback(filename, lines):
+ ... print filename, lines
+ ...
+ >>> l = LogWatcher("/var/www/example.com/logs", callback)
+ >>> l.loop()
+ """
+
+ def __init__(self, filelist, callback, extensions=["log"], tail_lines=0):
+ """Arguments:
+
+ (str) @folder:
+ the folder to watch
+
+ (callable) @callback:
+ a function which is called every time a new line in a
+ file being watched is found;
+ this is called with "filename" and "lines" arguments.
+
+ (list) @extensions:
+ only watch files with these extensions
+
+ (int) @tail_lines:
+ read last N lines from files being watched before starting
+ """
+ self.files_map = {}
+ self.filelist = filelist
+ self.callback = callback
+ # self.folder = os.path.realpath(folder)
+ self.extensions = extensions
+ # assert (os.path.isdir(self.folder), "%s does not exists"
+ # % self.folder)
+ for file in self.filelist:
+ assert (os.path.isfile(file))
+ assert callable(callback)
+ self.update_files()
+ # The first time we run the script we move all file markers at EOF.
+ # In case of files created afterwards we don't do this.
+ for id, file in list(iter(self.files_map.items())):
+ file.seek(os.path.getsize(file.name)) # EOF
+ if tail_lines:
+ lines = self.tail(file.name, tail_lines)
+ if lines:
+ self.callback(file.name, lines)
+
+ def __del__(self):
+ self.close()
+
+ def loop(self, interval=0.1, async=False):
+ """Start the loop.
+ If async is True make one loop then return.
+ """
+ while 1:
+ self.update_files()
+ for fid, file in list(iter(self.files_map.items())):
+ self.readfile(file)
+ if async:
+ return
+ time.sleep(interval)
+
+ def log(self, line):
+ """Log when a file is un/watched"""
+ print(line)
+
+ # def listdir(self):
+ # """List directory and filter files by extension.
+ # You may want to override this to add extra logic or
+ # globbling support.
+ # """
+ # ls = os.listdir(self.folder)
+ # if self.extensions:
+ # return ([x for x in ls if os.path.splitext(x)[1][1:]
+ # in self.extensions])
+ # else:
+ # return ls
+
+ @staticmethod
+ def tail(fname, window):
+ """Read last N lines from file fname."""
+ try:
+ f = open(fname, 'r')
+ except IOError as err:
+ if err.errno == errno.ENOENT:
+ return []
+ else:
+ raise
+ else:
+ BUFSIZ = 1024
+ f.seek(0, os.SEEK_END)
+ fsize = f.tell()
+ block = -1
+ data = ""
+ exit = False
+ while not exit:
+ step = (block * BUFSIZ)
+ if abs(step) >= fsize:
+ f.seek(0)
+ exit = True
+ else:
+ f.seek(step, os.SEEK_END)
+ data = f.read().strip()
+ if data.count('\n') >= window:
+ break
+ else:
+ block -= 1
+ return data.splitlines()[-window:]
+
+ def update_files(self):
+ ls = []
+ for name in self.filelist:
+ absname = os.path.realpath(os.path.join(name))
+ try:
+ st = os.stat(absname)
+ except EnvironmentError as err:
+ if err.errno != errno.ENOENT:
+ raise
+ else:
+ if not stat.S_ISREG(st.st_mode):
+ continue
+ fid = self.get_file_id(st)
+ ls.append((fid, absname))
+
+ # check existent files
+ for fid, file in list(iter(self.files_map.items())):
+ # next(iter(graph.items()))
+ try:
+ st = os.stat(file.name)
+ except EnvironmentError as err:
+ if err.errno == errno.ENOENT:
+ self.unwatch(file, fid)
+ else:
+ raise
+ else:
+ if fid != self.get_file_id(st):
+ # same name but different file (rotation); reload it.
+ self.unwatch(file, fid)
+ self.watch(file.name)
+
+ # add new ones
+ for fid, fname in ls:
+ if fid not in self.files_map:
+ self.watch(fname)
+
+ def readfile(self, file):
+ lines = file.readlines()
+ if lines:
+ self.callback(file.name, lines)
+
+ def watch(self, fname):
+ try:
+ file = open(fname, "r")
+ fid = self.get_file_id(os.stat(fname))
+ except EnvironmentError as err:
+ if err.errno != errno.ENOENT:
+ raise
+ else:
+ self.log("watching logfile %s" % fname)
+ self.files_map[fid] = file
+
+ def unwatch(self, file, fid):
+ # file no longer exists; if it has been renamed
+ # try to read it for the last time in case the
+ # log rotator has written something in it.
+ lines = self.readfile(file)
+ self.log("un-watching logfile %s" % file.name)
+ del self.files_map[fid]
+ if lines:
+ self.callback(file.name, lines)
+
+ @staticmethod
+ def get_file_id(st):
+ return "%xg%x" % (st.st_dev, st.st_ino)
+
+ def close(self):
+ for id, file in list(iter(self.files_map.items())):
+ file.close()
+ self.files_map.clear()
diff --git a/ee/core/models.py b/ee/core/models.py
new file mode 100644
index 00000000..2592a26c
--- /dev/null
+++ b/ee/core/models.py
@@ -0,0 +1,43 @@
+from sqlalchemy import Column, DateTime, String, Integer, Boolean, func
+from ee.core.database import Base
+
+
+class SiteDB(Base):
+ """
+ Databse model for site table
+ """
+ __tablename__ = 'sites'
+ id = Column(Integer, primary_key=True)
+ sitename = Column(String, unique=True)
+
+ site_type = Column(String)
+ cache_type = Column(String)
+ site_path = Column(String)
+
+ # Use default=func.now() to set the default created time
+ # of a site to be the current time when a
+ # Site record was created
+
+ created_on = Column(DateTime, default=func.now())
+ is_enabled = Column(Boolean, unique=False, default=True, nullable=False)
+ is_ssl = Column(Boolean, unique=False, default=False)
+ storage_fs = Column(String)
+ storage_db = Column(String)
+
+ def __init__(self, sitename=None, site_type=None, cache_type=None,
+ site_path=None, site_enabled=None,
+ is_ssl=None, storage_fs=None, storage_db=None):
+ self.sitename = sitename
+ self.site_type = site_type
+ self.cache_type = cache_type
+ self.site_path = site_path
+ self.is_enabled = site_enabled
+ self.is_ssl = is_ssl
+ self.storage_fs = storage_fs
+ self.storage_db = storage_db
+
+ # def __repr__(self):
+ # return '