Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Carlos Ramos Carreño
erp5
Commits
a271e8a8
Commit
a271e8a8
authored
Aug 19, 2021
by
Xavier Thompson
Browse files
Options
Browse Files
Download
Plain Diff
Theia Resiliency
See merge request
nexedi/slapos!931
parents
eac9043c
60f9d461
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
2179 additions
and
139 deletions
+2179
-139
software/theia/buildout.hash.cfg
software/theia/buildout.hash.cfg
+26
-2
software/theia/instance-export.cfg.jinja.in
software/theia/instance-export.cfg.jinja.in
+110
-0
software/theia/instance-import.cfg.jinja.in
software/theia/instance-import.cfg.jinja.in
+170
-0
software/theia/instance-resilient-input-schema.json
software/theia/instance-resilient-input-schema.json
+73
-0
software/theia/instance-resilient.cfg.jinja
software/theia/instance-resilient.cfg.jinja
+69
-0
software/theia/instance-theia.cfg.jinja.in
software/theia/instance-theia.cfg.jinja.in
+35
-11
software/theia/instance.cfg.in
software/theia/instance.cfg.in
+48
-0
software/theia/software.cfg
software/theia/software.cfg
+108
-47
software/theia/software.cfg.json
software/theia/software.cfg.json
+7
-0
software/theia/test/project_tests.py
software/theia/test/project_tests.py
+229
-0
software/theia/test/resilience_dummy/README.md
software/theia/test/resilience_dummy/README.md
+6
-0
software/theia/test/resilience_dummy/instance.cfg.in
software/theia/test/resilience_dummy/instance.cfg.in
+47
-0
software/theia/test/resilience_dummy/software.cfg
software/theia/test/resilience_dummy/software.cfg
+28
-0
software/theia/test/test.py
software/theia/test/test.py
+132
-75
software/theia/test/test_resiliency.py
software/theia/test/test_resiliency.py
+534
-0
software/theia/theia_common.py
software/theia/theia_common.py
+165
-0
software/theia/theia_export.py
software/theia/theia_export.py
+158
-0
software/theia/theia_import.py
software/theia/theia_import.py
+220
-0
stack/resilient/buildout.hash.cfg
stack/resilient/buildout.hash.cfg
+2
-2
stack/resilient/instance-pull-backup.cfg.in
stack/resilient/instance-pull-backup.cfg.in
+6
-1
stack/resilient/pbsready.cfg.in
stack/resilient/pbsready.cfg.in
+6
-1
No files found.
software/theia/buildout.hash.cfg
View file @
a271e8a8
...
...
@@ -15,11 +15,35 @@
[instance-theia]
_update_hash_filename_ = instance-theia.cfg.jinja.in
md5sum =
11d347dd2bf762902341746a388673a0
md5sum =
e0fb65c34b2d3d524cbca85579728e54
[instance]
_update_hash_filename_ = instance.cfg.in
md5sum = 063d3e19da9d3d4bfb77e8e638aa3a77
md5sum = a7d78b4002266c69ece05a476df82791
[instance-import]
_update_hash_filename_ = instance-import.cfg.jinja.in
md5sum = 861ef130f27175c2978a9b946b138dd5
[instance-export]
_update_hash_filename_ = instance-export.cfg.jinja.in
md5sum = b3cedaa1603ca8ed83fdd94ef4b35cc8
[instance-resilient]
_update_hash_filename_ = instance-resilient.cfg.jinja
md5sum = d78a9f885bdebf6720197209e0c21aa0
[theia-common]
_update_hash_filename_ = theia_common.py
md5sum = e57396473b4b6a17d26a747f0030293c
[theia-export]
_update_hash_filename_ = theia_export.py
md5sum = b5f5ac1924b27d3f2be2e5ea291c119e
[theia-import]
_update_hash_filename_ = theia_import.py
md5sum = 9e8c17a4b2d802695caf0c2c052f0d11
[yarn.lock]
_update_hash_filename_ = yarn.lock
...
...
software/theia/instance-export.cfg.jinja.in
0 → 100644
View file @
a271e8a8
{%- set parameter_dict = dict(default_parameter_dict, **parameter_dict) -%}
[buildout]
extends = {{ theia_instance_cfg }}
{{ pbsready_export_cfg }}
parts +=
monitor-base
$${:theia-parts}
$${:theia-environment-parts}
resilient-publish-connection-parameter
# The resilient stack makes the 'resilient' instance
# request the 'export' instance with a 'namebase' parameter.
# The export template then expects to receive it in
# slap-parameter:namebase
[slap-parameter]
namebase = {{ parameter_dict['namebase'] }}
# The resilient export stack periodically calls exporter:wrapper
# and then notifies the pull-backup instance that data is ready
# to be pulled from the export instance.
# All it expects is that a script be available in exporter:wrapper.
[exporter]
wrapper = $${theia-export-script:rendered}
[theia-export-script]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:bin}/theia-export-script
mode = 0700
exitcode-file = $${directory:srv}/export-exitcode-file
error-file = $${directory:srv}/export-errormessage-file
context =
raw python ${software-info:python-with-eggs}
raw theia_export ${software-info:theia-export}
raw bash ${software-info:bash}
raw rsync ${software-info:rsync}
raw sqlite3 ${software-info:sqlite3}
raw root_path $${buildout:directory}
raw backup_path $${directory:backup}
raw slapos_cfg $${directory:runner}/etc/slapos.cfg
raw project_path $${directory:project}
raw public_path $${directory:frontend-static-public}
key exitfile :exitcode-file
key errorfile :error-file
{%- raw %}
template =
inline:#!{{ bash }}
{{ python }} {{ theia_export }} \
--rsync {{ rsync }} \
--sqlite3 {{ sqlite3 }} \
--root {{ root_path }} \
--backup {{ backup_path }} \
--cfg {{ slapos_cfg }} \
--dirs {{ project_path }} \
--dirs {{ public_path }} \
--exitfile {{ exitfile }} \
--errorfile {{ errorfile }}
{%- endraw %}
# Add a promise to check that the export script has run
# successfully and recently (at most 2 days ago).
[promises]
export-promises =
$${export-promise:name}
[export-promise]
<= monitor-promise-base
module = check_command_execute
name = resiliency-export-promise.py
config-command = $${export-promise-script:rendered}
[initial-export-exitcode-file]
recipe = slapos.recipe.template:jinja2
rendered = $${theia-export-script:exitcode-file}
template = inline:0
once = $${:rendered}
[export-promise-script]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:bin}/export-promise-script
exitcode-file = $${initial-export-exitcode-file:rendered}
context =
key exitcodefile :exitcode-file
key errorfile theia-export-script:error-file
{%- raw %}
template =
inline:#!/bin/sh
if [ -z $(find {{ repr(exitcodefile) }} -mtime -2) ]
then
echo "ERROR export script last ran on " $(date -r {{ repr(exitcodefile) }})
exit 1
elif [ $( cat {{ repr(exitcodefile) }}) -ne 0 ]
then
echo "ERROR export script failed on " $(date -r {{ repr(exitcodefile) }})
cat {{ repr(errorfile) }}
exit 1
else
echo "OK export script last ran on " $(date -r {{ repr(exitcodefile) }})
exit 0
fi
{%- endraw %}
# Extend resilient parameters with normal theia connection parameters
[resilient-publish-connection-parameter]
<= publish-connection-parameter
software/theia/instance-import.cfg.jinja.in
0 → 100644
View file @
a271e8a8
{%- set parameter_dict = dict(default_parameter_dict, **parameter_dict) -%}
{%- set additional_frontend = parameter_dict['additional-frontend-guid'] -%}
[buildout]
extends = {{ theia_instance_cfg }}
{{ pbsready_import_cfg }}
parts +=
monitor-base
$${:theia-parts}
$${:theia-environment-parts}
# The resilient stack makes the 'resilient' instance
# request the 'import' instance with a 'namebase' parameter.
# The import template then expects to receive it in
# slap-parameter:namebase
[slap-parameter]
namebase = {{ parameter_dict['namebase'] }}
# Change frontend name to avoid conflicts
[remote-frontend]
name = Import {{ parameter_dict['frontend-name'] }}
{% if additional_frontend -%}
[remote-additional-frontend]
name = Import {{ parameter_dict['additional-frontend-name'] }}
{%- endif %}
# Change port ranges to avoid race conditions on port allocation
[frontend-instance-port]
minimum = 3200
maximum = 3300
[theia-service-port]
minimum = 3700
maximum = 3800
[slapos-standalone-port]
minimum = 4200
maximum = 4300
# Always disable autoprocessing in the import instance
[slapos-autorun]
autorun = stopped
# Change the gravatar favicon seed
[favicon.ico]
seed = Import {{ root_title }}
# The resilient stack calls post-notification-run:output followed by
# importer:wrapper when the instance is notified that the backup files
# have just been pushed to it. All it expects is the path of a script
# in post-notification-run:output and in importer:wrapper.
[post-notification-run]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:bin}/post-notification-run-script
output = $${:rendered}
mode = 0700
template =
inline:#!${software-info:bash}
# Do nothing because the backup signature will
# be verified by the import script itself
exit 0
[importer]
wrapper = $${theia-import-script:rendered}
[theia-import-script]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:bin}/theia-import-script
mode = 0700
exitcode-file = $${directory:srv}/import-exitcode-file
error-file = $${directory:srv}/import-errormessage-file
context =
raw python ${software-info:python-with-eggs}
raw theia_import ${software-info:theia-import}
raw bash ${software-info:bash}
raw rsync ${software-info:rsync}
raw sqlite3 ${software-info:sqlite3}
raw slapos ${software-info:slapos}
raw slapos_node_software_log $${directory:runner}/var/log/slapos-node-software.log
raw slapos_node_instance_log $${directory:runner}/var/log/slapos-node-instance.log
raw supervisorctl ${software-info:supervisorctl}
raw supervisord_conf $${directory:runner}/etc/supervisord.conf
raw root_path $${buildout:directory}
raw backup_path $${directory:backup}
raw slapos_cfg $${directory:runner}/etc/slapos.cfg
raw project_path $${directory:project}
raw public_path $${directory:frontend-static-public}
key exitfile :exitcode-file
key errorfile :error-file
{%- raw %}
template =
inline:#!{{ bash }}
. $${common-environment:rendered}
. $${slapos-standalone-activate:rendered}
{{ python }} {{ theia_import }} \
--rsync {{ rsync }} \
--sqlite3 {{ sqlite3 }} \
--slapos {{ slapos }} \
--srlog {{ slapos_node_software_log }} \
--cplog {{ slapos_node_instance_log }} \
--supervisorctl {{ supervisorctl }} \
--supervisordconf {{ supervisord_conf }} \
--root {{ root_path }} \
--backup {{ backup_path }} \
--cfg {{ slapos_cfg }} \
--dirs {{ project_path }} \
--dirs {{ public_path }} \
--exitfile {{ exitfile }} \
--errorfile {{ errorfile }}
{%- endraw %}
# Add a promise to check that the import script has run
# successfully and recently (at most 2 days ago).
[promises]
import-promises =
$${import-promise:name}
[import-promise]
<= monitor-promise-base
module = check_command_execute
name = resiliency-import-promise.py
config-command = $${import-promise-script:rendered}
[initial-import-exitcode-file]
recipe = slapos.recipe.template:jinja2
rendered = $${theia-import-script:exitcode-file}
template = inline:0
once = $${:rendered}
[import-promise-script]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:bin}/import-promise-script
exitcode-file = $${initial-import-exitcode-file:rendered}
context =
key exitcodefile :exitcode-file
key errorfile theia-import-script:error-file
{%- raw %}
template =
inline:#!/bin/sh
if [ -z $(find {{ repr(exitcodefile) }} -mtime -2) ]
then
echo "ERROR import script last ran on " $(date -r {{ repr(exitcodefile) }})
exit 1
elif [ $( cat {{ repr(exitcodefile) }}) -ne 0 ]
then
echo "ERROR import script failed on " $(date -r {{ repr(exitcodefile) }})
cat {{ repr(errorfile) }}
exit 1
else
echo "OK import script last ran on " $(date -r {{ repr(exitcodefile) }})
exit 0
fi
{%- endraw %}
# Resilient connection parameters of import instance are published
# through the resilient stack.
# Extend resilient parameters with normal theia connection parameters
[resilient-publish-connection-parameter]
<= publish-connection-parameter
software/theia/instance-resilient-input-schema.json
0 → 100644
View file @
a271e8a8
{
"$schema"
:
"http://json-schema.org/draft-04/schema"
,
"type"
:
"object"
,
"description"
:
"Parameters to instantiate resilient Theia"
,
"allOf"
:
[
{
"$ref"
:
"instance-input-schema.json#/"
},
{
"properties"
:
{
"resilient-clone-number"
:
{
"title"
:
"Amount of backup(s) to create"
,
"description"
:
"Amount of backup(s) to create. Each backup consists of a Pull Backup Server and a clone."
,
"type"
:
"integer"
,
"default"
:
1
,
"minimum"
:
0
,
"maximum"
:
2
,
"optional"
:
true
},
"-sla-theia0-computer_guid"
:
{
"title"
:
"Target computer for main instance"
,
"description"
:
"Target computer GUID for main instance."
,
"type"
:
"string"
,
"optional"
:
true
},
"-sla-theia1-computer_guid"
:
{
"title"
:
"Target computer for first clone"
,
"description"
:
"Target computer for first clone instance."
,
"type"
:
"string"
,
"optional"
:
true
},
"-sla-pbs1-computer_guid"
:
{
"title"
:
"Target computer for first PBS"
,
"description"
:
"Target computer for first PBS instance."
,
"type"
:
"string"
,
"optional"
:
true
},
"-sla-theia2-computer_guid"
:
{
"title"
:
"Target computer for second clone"
,
"description"
:
"Target computer for second clone instance."
,
"type"
:
"string"
,
"optional"
:
true
},
"-sla-pbs2-computer_guid"
:
{
"title"
:
"Target computer for second PBS"
,
"description"
:
"Target computer for second PBS instance."
,
"type"
:
"string"
,
"optional"
:
true
},
"resiliency-backup-periodicity"
:
{
"title"
:
"Periodicity of backup"
,
"description"
:
"Periodicity of backup, in cron format."
,
"type"
:
"string"
,
"optional"
:
true
},
"remove-backup-older-than"
:
{
"title"
:
"Remove backups older than..."
,
"description"
:
"Remove all the backups in PBS that are older than specified value. It should be rdiff-backup-compatible."
,
"type"
:
"string"
,
"default"
:
"2W"
,
"optional"
:
true
},
"ignore-known-hosts-file"
:
{
"title"
:
"Ignore known_hosts file"
,
"description"
:
"Set either to fill known_hosts file for ssh or not. Useful if main instance and PBS are using the same IP (slapos proxy, theia)."
,
"type"
:
"boolean"
,
"default"
:
false
,
"optional"
:
true
}
}
}
]
}
software/theia/instance-resilient.cfg.jinja
0 → 100644
View file @
a271e8a8
{% import 'parts' as parts %}
{% import 'replicated' as replicated with context %}
{% set number_of_instances = slapparameter_dict.get('resilient-clone-number', 1)|int %}
[buildout]
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
extends =
{{ monitor_template }}
parts +=
# Generate the parts to request theia-export, pull-backup and theia-import
# See stack/resilient/template-parts.cfg.in and stack/resilient/template-replicated.cfg.in
# See below for the generation of the sections corresponding to the parts generated here
{{ parts.replicate("theia", number_of_instances + 1) }}
# Also publish some connection parameters
publish-connection-parameter
[ArgLeader]
[ArgBackup]
# Generate sections to request theia-export, pull-backup and theia-import
# See stack/resilient/template-replicated.cfg.in
# In particular:
#
# [request-theia]
# <= ArgLeader
# software-type = export
# ...
#
# [request-theia-pseudo-replicating-1]
# <= ArgBackup
# software-type = import
# ...
#
# [request-pbs-theia-1]
# software-type = pull-backup
# ...
#
{{ replicated.replicate("theia", number_of_instances + 1,
"export", "import",
"ArgLeader", "ArgBackup",
slapparameter_dict=slapparameter_dict) }}
# Extend the list of return parameters for the export request
# The monitor parameters are only there to assert they are
# actually published by the export instance
[request-theia]
return += url username password backend-url monitor-base-url monitor-setup-url
# Extend the list of return parameters for the import request
# with the monitor parameters to assert they are actually published
[request-theia-pseudo-replicating-1]
return += monitor-base-url monitor-setup-url
# Publish some parameters from the export instance
[publish-connection-parameter]
recipe = slapos.cookbook:publish
url = ${request-theia:connection-url}
username = ${request-theia:connection-username}
password = ${request-theia:connection-password}
backend-url = ${request-theia:connection-backend-url}
# Publish resiliency parameters fetched by the resilient stack
[publish-connection-parameter]
<= publish-connection-information
software/theia/instance-theia.cfg.jinja.in
View file @
a271e8a8
...
...
@@ -41,23 +41,26 @@ backend-url = $${frontend-instance:url}
[directory]
recipe = slapos.cookbook:mkdirectory
etc = $${buildout:directory}/etc
var = $${buildout:directory}/var
srv = $${buildout:directory}/srv
bin = $${buildout:directory}/bin
tmp = $${buildout:directory}/tmp
dot-theia = $${buildout:directory}/.theia/
home = $${buildout:directory}
etc = $${:home}/etc
var = $${:home}/var
srv = $${:home}/srv
bin = $${:home}/bin
tmp = $${:home}/tmp
dot-theia = $${:home}/.theia/
pidfiles = $${:var}/run
services = $${:etc}/service
runner = $${:srv}/runner
backup = $${:srv}/backup/theia
project = $${:srv}/project
frontend-static = $${:srv}/frontend-static
frontend-static-public = $${:frontend-static}/public
frontend-static-css = $${:frontend-static}/css
bash-completions = $${
buildout:directory
}/.local/share/bash-completion/completions/
fish-completions = $${
buildout:directory
}/.config/fish/completions/
bash-completions = $${
:home
}/.local/share/bash-completion/completions/
fish-completions = $${
:home
}/.config/fish/completions/
# Promises
...
...
@@ -68,11 +71,13 @@ recipe =
instance-promises =
$${theia-listen-promise:name}
$${frontend-listen-promise:name}
$${frontend-authentification-promise:name}
$${remote-frontend-url-available-promise:name}
{% if additional_frontend %}
$${remote-additional-frontend-url-available-promise:name}
{% endif %}
$${slapos-standalone-listen-promise:name}
$${slapos-standalone-ready-promise:name}
$${slapos-autorun-promise:name}
[theia-listen-promise]
...
...
@@ -89,6 +94,16 @@ name = $${:_buildout_section_name_}.py
config-host = $${frontend-instance:ip}
config-port = $${frontend-instance:port}
[frontend-authentification-promise]
<= monitor-promise-base
module = check_url_available
name = $${:_buildout_section_name_}.py
username = $${frontend-instance-password:username}
password = $${frontend-instance-password:passwd}
ip = $${frontend-instance:ip}
port = $${frontend-instance:port}
config-url = https://$${:username}:$${:password}@[$${:ip}]:$${:port}
[remote-frontend-url-available-promise]
<= monitor-promise-base
module = check_url_available
...
...
@@ -113,13 +128,19 @@ name = standalone-listen-promise.py
config-host = $${slapos-standalone-instance:hostname}
config-port = $${slapos-standalone-instance:port}
[slapos-standalone-ready-promise]
<= monitor-promise-base
module = check_socket_listening
name = standalone-ready-promise.py
config-abstract = $${directory:runner}/standalone_ready
[slapos-autorun-promise]
<= monitor-promise-base
module = check_service_state
# XXX promise plugins can not contain "slapos" in their names
name = autorun-state-promise.py
config-service = $${slapos-autorun:service-name}
config-expect = $${slapos-autorun:autorun}
config-run-directory = $${directory:runner}/var/run
# Remote Caddy Frontend
...
...
@@ -279,13 +300,14 @@ wait-for-files = $${frontend-instance:pidfile}
[favicon.ico]
# generate a pseudo random favicon, different for each instance name.
recipe = slapos.recipe.build
seed = {{ root_title }}
install =
import hashlib, shutil
buildout_offline = self.buildout['buildout']['offline']
self.buildout['buildout']['offline'] = 'false'
try:
gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(
b'''
{{ root_title }
}'''
b'''
$${:seed
}'''
).hexdigest() + "?s=256&d=retro"
shutil.copy(self.download(gravatar_url), '''$${:location}''')
except Exception:
...
...
@@ -310,7 +332,7 @@ mode = 0700
template =
inline:
#!/bin/sh
export HOME=$${
buildout:directory
}
export HOME=$${
directory:home
}
export PATH=${python-language-server:location}/bin:${java-jdk:location}/bin:${cli-utilities:PATH}:$HOME/.cargo/bin:$PATH
...
...
@@ -419,6 +441,7 @@ ip = {{ ipv4_random }}
ipv4 = {{ ipv4_random }}
ipv6 = {{ ipv6_random }}
port = $${slapos-standalone-port:port}
local-software-release-root = $${directory:home}
slapos-configuration = $${directory:runner}/etc/slapos.cfg
computer-id = slaprunner
...
...
@@ -447,6 +470,7 @@ template =
$${slapos-standalone-config:ipv4} \
$${slapos-standalone-config:ipv6} \
$${slapos-standalone-config:port} \
$${slapos-standalone-config:local-software-release-root} \
$${slapos-standalone-config:computer-id} \
{%- if parameter_dict.get('embedded-sr') %}
--sr='{{ parameter_dict['embedded-sr'] }}' \
...
...
software/theia/instance.cfg.in
View file @
a271e8a8
...
...
@@ -2,6 +2,9 @@
parts =
switch-softwaretype
extends =
${template-resilient-templates:output}
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
...
...
@@ -18,6 +21,11 @@ recipe = slapos.cookbook:switch-softwaretype
RootSoftwareInstance = $${:default}
default = $${:theia}
theia = theia:rendered
export = export:rendered
import = import:rendered
resilient = resilient:rendered
frozen = instance-frozen:rendered
pull-backup = template-pull-backup:rendered
[theia]
recipe = slapos.recipe.template:jinja2
...
...
@@ -46,3 +54,43 @@ default-parameters =
"additional-frontend-guid": null
}
frontend-sr = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
[import]
recipe = slapos.recipe.template:jinja2
template = ${instance-import:output}
rendered = $${buildout:directory}/instance-import.cfg
mode = 0644
context =
jsonkey default_parameter_dict theia:default-parameters
key parameter_dict slap-configuration:configuration
key theia_instance_cfg theia:rendered
key pbsready_import_cfg template-pbsready-import:rendered
key root_title slap-configuration:root-instance-title
[export]
recipe = slapos.recipe.template:jinja2
template = ${instance-export:output}
rendered = $${buildout:directory}/instance-export.cfg
mode = 0644
context =
jsonkey default_parameter_dict theia:default-parameters
key parameter_dict slap-configuration:configuration
key theia_instance_cfg theia:rendered
key pbsready_export_cfg template-pbsready-export:rendered
[resilient]
recipe = slapos.recipe.template:jinja2
template = ${instance-resilient:output}
rendered = $${buildout:directory}/instance-resilient.cfg
mode = 0644
extensions = jinja2.ext.do
context =
key buildout buildout:bin-directory
key develop_eggs_directory buildout:develop-eggs-directory
key eggs_directory buildout:eggs-directory
key slapparameter_dict slap-configuration:configuration
raw monitor_template ${monitor-template:rendered}
template-parts-destination = ${template-parts:target}
template-replicated-destination = ${template-replicated:target}
import-list = file parts :template-parts-destination
file replicated :template-replicated-destination
software/theia/software.cfg
View file @
a271e8a8
...
...
@@ -15,6 +15,7 @@ extends =
../../stack/nodejs.cfg
../../stack/slapos.cfg
../../stack/monitor/buildout.cfg
../../stack/resilient/buildout.cfg
../../component/defaults.cfg
./download-plugins.cfg
./buildout.hash.cfg
...
...
@@ -22,8 +23,14 @@ extends =
parts =
theia-wrapper
slapos-cookbook
python-with-eggs
instance-theia
instance
instance-import
instance-export
instance-resilient
theia-common
theia-export
# default for slapos-standalone
shared-part-list =
...
...
@@ -54,9 +61,10 @@ initialization =
import argparse
import glob
import json
import os.path
import sys
import os
import signal
import socket
import sys
import time
import slapos.slap.standalone
...
...
@@ -66,6 +74,7 @@ initialization =
parser.add_argument('ipv4')
parser.add_argument('ipv6')
parser.add_argument('server_port', type=int)
parser.add_argument('local_software_release_root')
parser.add_argument('computer_id')
parser.add_argument('--sr')
parser.add_argument('--srtype')
...
...
@@ -101,58 +110,64 @@ initialization =
instance_root="%s/instance" % args.base_directory,
partition_forward_configuration=partition_forward_configuration,
slapos_bin="${buildout:bin-directory}/slapos",
local_software_release_root=args.local_software_release_root,
)
standalone.start()
partition_count = 20
if len(glob.glob(os.path.join(standalone.instance_directory, '*'))) < partition_count:
print("Standalone SlapOS: Formatting {partition_count} partitions".format(
partition_count=partition_count))
standalone.format(
partition_count,
args.ipv4,
args.ipv6
)
print("Standalone SlapOS for computer `{}` started".format(args.computer_id))
# Run instance at least once, to start the supervisor managing instances.
try:
standalone.waitForInstance(max_retry=0)
except slapos.slap.standalone.SlapOSNodeCommandError as e:
print("Error instanciating: {}".format(e))
if args.sr:
try:
with open(args.srparams) as f:
params = json.load(f)
except Exception:
params = None
if not isinstance(params, dict):
params = None
print("Supplying and Requesting Embedded Software {sr} with type {srtype}".format(
sr=args.sr, srtype=args.srtype))
print("With parameters {param_dict} parsed from '{srparams}'".format(
param_dict=params, srparams=args.srparams))
standalone.supply(args.sr)
standalone.request(
args.sr,
"Embedded Instance",
args.srtype,
partition_parameter_kw=params,
)
quit_requested = []
def signal_handler(signum, frame):
print("Signal {signum} received".format(signum=signum))
quit_requested.append(True
)
sys.exit(
)
signal.signal(signal.SIGTERM, signal_handler)
print("Standalone SlapOS ready")
while not quit_requested:
time.sleep(.1)
standalone.start()
try:
partition_count = 20
if len(glob.glob(os.path.join(standalone.instance_directory, '*'))) < partition_count:
print("Standalone SlapOS: Formatting {partition_count} partitions".format(
partition_count=partition_count))
standalone.format(
partition_count,
args.ipv4,
args.ipv6,
)
print("Standalone SlapOS for computer `{}` started".format(args.computer_id))
# Run instance at least once, to start the supervisor managing instances.
try:
standalone.waitForInstance(max_retry=0)
except slapos.slap.standalone.SlapOSNodeCommandError as e:
print("Error instanciating: {}".format(e))
if args.sr:
try:
with open(args.srparams) as f:
params = json.load(f)
except Exception:
params = None
if not isinstance(params, dict):
params = None
print("Supplying and Requesting Embedded Software {sr} with type {srtype}".format(
sr=args.sr, srtype=args.srtype))
print("With parameters {param_dict} parsed from '{srparams}'".format(
param_dict=params, srparams=args.srparams))
standalone.supply(args.sr)
standalone.request(
args.sr,
"Embedded Instance",
args.srtype,
partition_parameter_kw=params,
)
s = socket.socket(socket.AF_UNIX)
s.bind('\0' + os.path.join(args.base_directory, 'standalone_ready'))
s.listen(5)
print("Standalone SlapOS ready")
while True:
s.accept()[0].close()
finally:
print("Stopping standalone subsystem")
standalone.stop()
print("Exiting")
print("Stopping standalone subsystem")
standalone.stop()
print("Exiting")
sys.exit(0)
needs-these-eggs-scripts-in-path =
${supervisor:recipe}
...
...
@@ -343,6 +358,18 @@ template =
#!/bin/sh
exec ${nodejs:location}/bin/node ${theia:location}/node_modules/.bin/theia-open "$@"
[python-with-eggs]
recipe = zc.recipe.egg
interpreter = ${:_buildout_section_name_}
eggs =
${slapos-toolbox:eggs}
six
zc.buildout
# Only generate the interpreter script to avoid conflicts with scripts
# for eggs that are also generated by another section, like slapos.toolbox
scripts = ${:interpreter}
[instance-theia]
<= template-base
output = ${buildout:directory}/instance-theia.cfg.jinja
...
...
@@ -350,3 +377,37 @@ output = ${buildout:directory}/instance-theia.cfg.jinja
[instance]
<= template-base
output = ${buildout:directory}/instance.cfg
[instance-import]
<= template-base
output = ${buildout:directory}/instance-import.cfg.jinja
[instance-export]
<= template-base
output = ${buildout:directory}/instance-export.cfg.jinja
[instance-resilient]
<= download-base
[theia-common]
<= download-base
destination = ${buildout:directory}/theia_common.py
[theia-export]
<= download-base
destination = ${buildout:directory}/theia_export.py
[theia-import]
<= download-base
destination = ${buildout:directory}/theia_import.py
[software-info]
slapos = ${buildout:bin-directory}/slapos
python-with-eggs = ${buildout:bin-directory}/${python-with-eggs:interpreter}
python = ${python:location}/bin/python
rsync = ${rsync:location}/bin/rsync
sqlite3 = ${sqlite3:location}/bin/sqlite3
bash = ${bash:location}/bin/bash
supervisorctl = ${buildout:bin-directory}/supervisorctl
theia-export = ${theia-export:output}
theia-import = ${theia-import:output}
software/theia/software.cfg.json
View file @
a271e8a8
...
...
@@ -9,6 +9,13 @@
"description"
:
"Default"
,
"request"
:
"instance-input-schema.json"
,
"response"
:
"instance-output-schema.json"
,
"index"
:
0
},
"resilient"
:
{
"title"
:
"Resilient"
,
"description"
:
"Resilient Theia"
,
"request"
:
"instance-resilient-input-schema.json"
,
"response"
:
"instance-output-schema.json"
,
"index"
:
1
}
}
...
...
software/theia/test/project_tests.py
0 → 100644
View file @
a271e8a8
##############################################################################
#
# Copyright (c) 2019 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from
__future__
import
unicode_literals
import
gzip
import
json
import
os
import
re
import
subprocess
import
time
import
unittest
import
requests
from
datetime
import
datetime
,
timedelta
from
six.moves.urllib.parse
import
urljoin
from
slapos.testing.testcase
import
installSoftwareUrlList
import
test_resiliency
from
test
import
SlapOSInstanceTestCase
,
theia_software_release_url
erp5_software_release_url
=
os
.
path
.
abspath
(
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'..'
,
'..'
,
'erp5'
,
'software.cfg'
))
def
setUpModule
():
installSoftwareUrlList
(
SlapOSInstanceTestCase
,
[
theia_software_release_url
,
erp5_software_release_url
],
debug
=
bool
(
int
(
os
.
environ
.
get
(
'SLAPOS_TEST_DEBUG'
,
0
))),
)
class
ERP5Mixin
(
object
):
_test_software_url
=
erp5_software_release_url
_connexion_parameters_regex
=
re
.
compile
(
r"{\
s*
'_'\
s*:
\s*'(.*)'\
s*}
")
def _getERP5ConnexionParameters(self, software_type='export'):
slapos = self._getSlapos(software_type)
out = subprocess.check_output(
(slapos, 'request', 'test_instance', self._test_software_url),
stderr=subprocess.STDOUT,
)
print(out)
return json.loads(self._connexion_parameters_regex.search(out).group(1))
def _getERP5Url(self, connexion_parameters, path=''):
return urljoin(connexion_parameters['family-default-v6'], path)
def _getERP5User(self, connexion_parameters):
return connexion_parameters['inituser-login']
def _getERP5Password(self, connexion_parameters):
return connexion_parameters['inituser-password']
def _waitERP5connected(self, url, user, password):
for _ in range(5):
try:
resp = requests.get('%s/getId' % url, auth=(user, password), verify=False, allow_redirects=False)
except Exception:
time.sleep(20)
continue
if resp.status_code != 200:
time.sleep(20)
continue
break
else:
self.fail('Failed to connect to ERP5')
self.assertEqual(resp.text, 'erp5')
def _getERP5Partition(self, servicename):
p = subprocess.Popen(
(self._getSlapos(), 'node', 'status'),
stdout=subprocess.PIPE, universal_newlines=True)
out, _ = p.communicate()
found = set()
for line in out.splitlines():
if servicename in line:
found.add(line.split(':')[0])
if not found:
raise Exception("
ERP5
%
s
partition
not
found
" % servicename)
elif len(found) > 1:
raise Exception("
Found
several
partitions
for
ERP5
%
s
" % servicename)
return found.pop()
def _getERP5PartitionPath(self, software_type, servicename, *paths):
partition = self._getERP5Partition(servicename)
return self._getPartitionPath(
software_type, 'srv', 'runner', 'instance', partition, *paths)
class TestTheiaResilienceERP5(ERP5Mixin, test_resiliency.TestTheiaResilience):
test_instance_max_retries = 12
backup_max_tries = 480
backup_wait_interval = 60
def _prepareExport(self):
super(TestTheiaResilienceERP5, self)._prepareExport()
# Connect to erp5
info = self._getERP5ConnexionParameters()
user = self._getERP5User(info)
password = self._getERP5Password(info)
url = self._getERP5Url(info, 'erp5')
self._waitERP5connected(url, user, password)
# Change title
new_title = time.strftime("
HelloTitle
%
a
%
d
%
b
%
Y
%
H
:
%
M
:
%
S
", time.localtime(time.time()))
requests.get('%s/portal_types/setTitle?value=%s' % (url, new_title), auth=(user, password), verify=False)
resp = requests.get('%s/portal_types/getTitle' % url, auth=(user, password), verify=False, allow_redirects=False)
self.assertEqual(resp.text, new_title)
self._erp5_new_title = new_title
# Wait until changes have been catalogued
mariadb_partition = self._getERP5PartitionPath('export', 'mariadb')
mysql_bin = os.path.join(mariadb_partition, 'bin', 'mysql')
wait_activities_script = os.path.join(
mariadb_partition, 'software_release', 'parts', 'erp5',
'Products', 'CMFActivity', 'bin', 'wait_activities')
subprocess.check_call((wait_activities_script, 'erp5'), env={'MYSQL': mysql_bin})
# Check that changes have been catalogued
output = subprocess.check_output((mysql_bin, 'erp5', '-e', 'SELECT title FROM catalog WHERE id="
portal_types
"'))
self.assertIn(new_title, output)
# Compute backup date in the near future
soon = (datetime.now() + timedelta(minutes=4)).replace(second=0)
date = '*:%d:00' % soon.minute
params = '_={"
zodb
-
zeo
": {"
backup
-
periodicity
": "
%
s
"}, "
mariadb
": {"
backup
-
periodicity
": "
%
s
"} }' % (date, date)
# Update ERP5 parameters
print('Requesting ERP5 with parameters %s' % params)
slapos = self._getSlapos()
subprocess.check_call((slapos, 'request', 'test_instance', self._test_software_url, '--parameters', params))
# Process twice to propagate parameter changes
for _ in range(2):
subprocess.check_call((slapos, 'node', 'instance'))
# Restart cron (actually all) services to let them take the new date into account
# XXX this should not be required, updating ERP5 parameters should be enough
subprocess.call((slapos, 'node', 'restart', 'all'))
# Wait until after the programmed backup date, and a bit more
t = (soon - datetime.now()).total_seconds()
self.assertLess(0, t)
time.sleep(t + 120)
# Check that mariadb backup has started
mariadb_backup = os.path.join(mariadb_partition, 'srv', 'backup', 'mariadb-full')
mariadb_backup_dump, = os.listdir(mariadb_backup)
# Check that zodb backup has started
zodb_backup = self._getERP5PartitionPath('export', 'zeo', 'srv', 'backup', 'zodb', 'root')
self.assertEqual(len(os.listdir(zodb_backup)), 3)
# Check that mariadb catalog backup contains expected changes
with gzip.open(os.path.join(mariadb_backup, mariadb_backup_dump)) as f:
self.assertIn(new_title, f.read(), "
Mariadb
catalog
backup
%
s
is
not
up
to
date
" % mariadb_backup_dump)
def _checkTakeover(self):
super(TestTheiaResilienceERP5, self)._checkTakeover()
# Connect to erp5
info = self._getERP5ConnexionParameters()
user = self._getERP5User(info)
password = self._getERP5Password(info)
url = self._getERP5Url(info, 'erp5')
self._waitERP5connected(url, user, password)
resp = requests.get('%s/portal_types/getTitle' % url, auth=(user, password), verify=False, allow_redirects=False)
self.assertEqual(resp.text, self._erp5_new_title)
# Check that the mariadb catalog is not yet restored
mariadb_partition = self._getERP5PartitionPath('export', 'mariadb')
mysql_bin = os.path.join(mariadb_partition, 'bin', 'mysql')
query = 'SELECT title FROM catalog WHERE id="
portal_types
"'
try:
out = subprocess.check_output((mysql_bin, 'erp5', '-e', query))
except subprocess.CalledProcessError:
out = ''
self.assertNotIn(self._erp5_new_title, out)
# Stop all services
slapos = self._getSlapos()
print("
Stop
all
services
")
subprocess.call((slapos, 'node', 'stop', 'all'))
# Manually restore mariadb from backup
mariadb_restore_script = os.path.join(mariadb_partition, 'bin', 'restore-from-backup')
print("
Restore
mariadb
from
backup
")
subprocess.check_call(mariadb_restore_script)
# Check that the test instance is properly redeployed after restoring mariadb
# This restarts the services and checks the promises of the test instance
# Process twice to propagate state change
for _ in range(2):
self._processEmbeddedInstance(self.test_instance_max_retries)
# Check that the mariadb catalog was properly restored
out = subprocess.check_output((mysql_bin, 'erp5', '-e', query))
self.assertIn(self._erp5_new_title, out, 'Mariadb catalog is not properly restored')
software/theia/test/resilience_dummy/README.md
0 → 100644
View file @
a271e8a8
# Resilience Dummy Software
A very simple SR to test resiliency:
-
fast installation and deployment
-
self-contained - no dependency outside this folder
-
has a simple
`exporter.exclude`
and
`runner-import-restore`
software/theia/test/resilience_dummy/instance.cfg.in
0 → 100644
View file @
a271e8a8
[buildout]
parts =
log-writer
exporter.exclude
runner-import-restore
backup-identity-script
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[directory]
recipe = plone.recipe.command
home = $${buildout:directory}
srv = $${:home}/srv
etc = $${:home}/etc
run = $${:etc}/run
command = mkdir -p $${:run} $${:srv}
[log-writer]
recipe = slapos.recipe.template:jinja2
template = inline:#!/bin/sh
echo "Hello : $(date)" >> $${directory:home}/log.log
rendered = $${directory:run}/log-writer
[exporter.exclude]
recipe = slapos.recipe.template:jinja2
template = inline:$${directory:home}/exclude
rendered = $${directory:srv}/exporter.exclude
[runner-import-restore]
recipe = slapos.recipe.template:jinja2
template = inline:#!/bin/sh
echo "Hello : $(date)" >> $${directory:home}/runner-import-restore.log
exit $TEST_RESTORE_STATUS
rendered = $${directory:srv}/runner-import-restore
[backup-identity-script]
recipe = slapos.recipe.template:jinja2
template = inline:#!/bin/sh
echo "Custom script"
for i in "$@"
do
echo $(sha256sum $i)
done
exit $TEST_BACKUP_STATUS
rendered = $${directory:srv}/.backup_identity_script
software/theia/test/resilience_dummy/software.cfg
0 → 100644
View file @
a271e8a8
[buildout]
find-links +=
http://www.nexedi.org/static/packages/source/
http://www.nexedi.org/static/packages/source/slapos.buildout/
parts =
instance-template
plone-recipe-command
versions = versions
[plone-recipe-command]
recipe = zc.recipe.egg
eggs = plone.recipe.command
[instance-template]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance.cfg.in
output = ${buildout:directory}/instance.cfg
mode = 0644
[versions]
setuptools = 44.0.0
zc.buildout = 2.7.1+slapos010
zc.recipe.egg = 2.0.3+slapos003
Jinja2 = 2.11.2
MarkupSafe = 1.0
software/theia/test/test.py
View file @
a271e8a8
...
...
@@ -26,31 +26,28 @@
##############################################################################
from
__future__
import
unicode_literals
import
os
import
textwrap
import
json
import
logging
import
os
import
re
import
subprocess
import
tempfile
import
time
import
re
import
json
from
six.moves.urllib.parse
import
urlparse
,
urljoin
import
pexpect
import
psutil
import
requests
import
sqlite3
import
six
from
six.moves.urllib.parse
import
urlparse
,
urljoin
from
slapos.testing.testcase
import
makeModuleSetUpAndTestCaseClass
from
slapos.grid.svcbackend
import
getSupervisorRPC
from
slapos.grid.svcbackend
import
_getSupervisordSocketPath
from
slapos.grid.svcbackend
import
getSupervisorRPC
,
_getSupervisordSocketPath
software_cfg
=
'software%s.cfg'
%
(
'-py3'
if
six
.
PY3
else
''
)
setUpModule
,
SlapOSInstanceTestCase
=
makeModuleSetUpAndTestCaseClass
(
os
.
path
.
abspath
(
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'..'
,
software_cfg
))
)
theia_software_release_url
=
os
.
path
.
abspath
(
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
'..'
,
software_cfg
))
setUpModule
,
SlapOSInstanceTestCase
=
makeModuleSetUpAndTestCaseClass
(
theia_software_release_url
)
class
TheiaTestCase
(
SlapOSInstanceTestCase
):
...
...
@@ -67,12 +64,21 @@ class TestTheia(TheiaTestCase):
def
setUp
(
self
):
self
.
connection_parameters
=
self
.
computer_partition
.
getConnectionParameterDict
()
def
get
(
self
,
url
,
expect_code
=
requests
.
codes
.
ok
):
resp
=
requests
.
get
(
url
,
verify
=
False
)
self
.
assertEqual
(
expect_code
,
resp
.
status_code
,
'%s returned %d instead of %d'
%
(
url
,
resp
.
status_code
,
expect_code
),
)
return
resp
def
test_backend_http_get
(
self
):
resp
=
requests
.
get
(
self
.
connection_parameters
[
'backend-url'
],
verify
=
False
)
self
.
assertEqual
(
requests
.
codes
.
unauthorized
,
resp
.
status_code
)
backend_url
=
self
.
connection_parameters
[
'backend-url'
]
self
.
get
(
backend_url
,
requests
.
codes
.
unauthorized
)
# with login/password, this is allowed
parsed_url
=
urlparse
(
self
.
connection_parameters
[
'backend-url'
]
)
parsed_url
=
urlparse
(
backend_url
)
authenticated_url
=
parsed_url
.
_replace
(
netloc
=
'{}:{}@[{}]:{}'
.
format
(
self
.
connection_parameters
[
'username'
],
...
...
@@ -80,12 +86,11 @@ class TestTheia(TheiaTestCase):
parsed_url
.
hostname
,
parsed_url
.
port
,
)).
geturl
()
resp
=
requests
.
get
(
authenticated_url
,
verify
=
False
)
self
.
assertEqual
(
requests
.
codes
.
ok
,
resp
.
status_code
)
self
.
get
(
authenticated_url
)
def
test_http_get
(
self
):
resp
=
requests
.
get
(
self
.
connection_parameters
[
'url'
],
verify
=
False
)
self
.
assertEqual
(
requests
.
codes
.
unauthorized
,
resp
.
status_code
)
url
=
self
.
connection_parameters
[
'url'
]
self
.
get
(
url
,
requests
.
codes
.
unauthorized
)
# with login/password, this is allowed
parsed_url
=
urlparse
(
self
.
connection_parameters
[
'url'
])
...
...
@@ -96,33 +101,28 @@ class TestTheia(TheiaTestCase):
parsed_url
.
hostname
,
parsed_url
.
port
,
)).
geturl
()
resp
=
requests
.
get
(
authenticated_url
,
verify
=
False
)
self
.
assertEqual
(
requests
.
codes
.
ok
,
resp
.
status_code
)
self
.
get
(
authenticated_url
)
# there's a public folder to serve file
with
open
(
'{}/srv/frontend-static/public/test_file'
.
format
(
self
.
computer_partition_root_path
),
'w'
)
as
f
:
f
.
write
(
"hello"
)
resp
=
requests
.
get
(
urljoin
(
authenticated_url
,
'/public/'
),
verify
=
False
)
resp
=
self
.
get
(
urljoin
(
authenticated_url
,
'/public/'
)
)
self
.
assertIn
(
'test_file'
,
resp
.
text
)
resp
=
requests
.
get
(
urljoin
(
authenticated_url
,
'/public/test_file'
),
verify
=
False
)
resp
=
self
.
get
(
urljoin
(
authenticated_url
,
'/public/test_file'
))
self
.
assertEqual
(
'hello'
,
resp
.
text
)
# there's a (not empty) favicon
resp
=
requests
.
get
(
urljoin
(
authenticated_url
,
'/favicon.ico'
),
verify
=
False
)
self
.
assertEqual
(
requests
.
codes
.
ok
,
resp
.
status_code
)
resp
=
self
.
get
(
urljoin
(
authenticated_url
,
'/favicon.ico'
))
self
.
assertTrue
(
resp
.
raw
)
# there is a CSS referencing fonts
css_text
=
requests
.
get
(
urljoin
(
authenticated_url
,
'/css/slapos.css'
),
verify
=
False
).
text
css_text
=
self
.
get
(
urljoin
(
authenticated_url
,
'/css/slapos.css'
)
).
text
css_urls
=
re
.
findall
(
r'url\
([
\'"]+([^
\
)]+)[
\
'"
]
+
\
)
', css_text)
self.assertTrue(css_urls)
# and fonts are served
for url in css_urls:
resp = requests.get(urljoin(authenticated_url, url), verify=False)
self.assertEqual(requests.codes.ok, resp.status_code)
resp = self.get(urljoin(authenticated_url, url))
self.assertTrue(resp.raw)
def test_theia_slapos(self):
...
...
@@ -281,7 +281,7 @@ class TestTheiaEnv(TheiaTestCase):
}
def
test_theia_env
(
self
):
"""Make sure environment variables are the same wether we use shell or supervisor services.
"""Make sure environment variables are the same w
h
ether we use shell or supervisor services.
"""
# The path of the env.json file expected to be generated by building the dummy software release
env_json_path
=
os
.
path
.
join
(
self
.
computer_partition_root_path
,
'srv'
,
'runner'
,
'software'
,
'env.json'
)
...
...
@@ -303,47 +303,104 @@ class TestTheiaEnv(TheiaTestCase):
# Start a theia shell that inherits the environment of the theia process
# This simulates the environment of a shell launched from the browser application
theia_shell_process
=
pexpect
.
spawnu
(
'{}/bin/theia-shell'
.
format
(
self
.
computer_partition_root_path
),
env
=
theia_env
)
theia_shell_process
.
expect_exact
(
'Standalone SlapOS for computer `slaprunner` activated'
)
# Launch slapos node software from theia shell
theia_shell_process
.
sendline
(
'slapos node software'
)
theia_shell_process
.
expect
(
'Installing software release %s'
%
self
.
dummy_software_path
)
theia_shell_process
.
expect
(
'Finished software releases.'
)
# Get the theia shell environment
with
open
(
env_json_path
)
as
f
:
theia_shell_env
=
json
.
load
(
f
)
# Remove the env.json file to later be sure that a new one has been generated
os
.
remove
(
env_json_path
)
# Launch slapos node software service from the embedded supervisord.
# Note that we have two services, slapos-not-software and slapos-not-software-all
# The later uses --all which is what we want to use here, because the software
# is already installed and we want to install it again, this time from supervisor
embedded_run_path
=
os
.
path
.
join
(
self
.
computer_partition_root_path
,
'srv'
,
'runner'
,
'var'
,
'run'
)
embedded_supervisord_socket_path
=
_getSupervisordSocketPath
(
embedded_run_path
,
self
.
logger
)
with
getSupervisorRPC
(
embedded_supervisord_socket_path
)
as
embedded_supervisor
:
previous_stop_time
=
embedded_supervisor
.
getProcessInfo
(
'slapos-node-software-all'
)[
'stop'
]
embedded_supervisor
.
startProcess
(
'slapos-node-software-all'
)
for
_retries
in
range
(
20
):
time
.
sleep
(
1
)
if
embedded_supervisor
.
getProcessInfo
(
'slapos-node-software-all'
)[
'stop'
]
!=
previous_stop_time
:
break
else
:
self
.
fail
(
"the supervisord service 'slapos-node-software-all' takes too long to finish"
)
# Get the supervisord environment
with
open
(
env_json_path
)
as
f
:
supervisord_env
=
json
.
load
(
f
)
# Compare relevant variables from both environments
self
.
maxDiff
=
None
self
.
assertEqual
(
theia_shell_env
[
'PATH'
].
split
(
':'
),
supervisord_env
[
'PATH'
].
split
(
':'
))
self
.
assertEqual
(
theia_shell_env
[
'SLAPOS_CONFIGURATION'
],
supervisord_env
[
'SLAPOS_CONFIGURATION'
])
self
.
assertEqual
(
theia_shell_env
[
'SLAPOS_CLIENT_CONFIGURATION'
],
supervisord_env
[
'SLAPOS_CLIENT_CONFIGURATION'
])
self
.
assertEqual
(
theia_shell_env
[
'HOME'
],
supervisord_env
[
'HOME'
])
# Cleanup the theia shell process
theia_shell_process
.
terminate
()
theia_shell_process
.
wait
()
try
:
theia_shell_process
.
expect_exact
(
'Standalone SlapOS for computer `slaprunner` activated'
)
# Launch slapos node software from theia shell
theia_shell_process
.
sendline
(
'slapos node software'
)
theia_shell_process
.
expect
(
'Installing software release %s'
%
self
.
dummy_software_path
)
theia_shell_process
.
expect
(
'Finished software releases.'
)
# Get the theia shell environment
with
open
(
env_json_path
)
as
f
:
theia_shell_env
=
json
.
load
(
f
)
# Remove the env.json file to later be sure that a new one has been generated
os
.
remove
(
env_json_path
)
# Launch slapos node software service from the embedded supervisord.
# Note that we have two services, slapos-node-software and slapos-node-software-all
# The later uses --all which is what we want to use here, because the software
# is already installed and we want to install it again, this time from supervisor
embedded_run_path
=
os
.
path
.
join
(
self
.
computer_partition_root_path
,
'srv'
,
'runner'
,
'var'
,
'run'
)
embedded_supervisord_socket_path
=
_getSupervisordSocketPath
(
embedded_run_path
,
self
.
logger
)
with
getSupervisorRPC
(
embedded_supervisord_socket_path
)
as
embedded_supervisor
:
previous_stop_time
=
embedded_supervisor
.
getProcessInfo
(
'slapos-node-software-all'
)[
'stop'
]
embedded_supervisor
.
startProcess
(
'slapos-node-software-all'
)
for
_retries
in
range
(
20
):
time
.
sleep
(
1
)
if
embedded_supervisor
.
getProcessInfo
(
'slapos-node-software-all'
)[
'stop'
]
!=
previous_stop_time
:
break
else
:
self
.
fail
(
"the supervisord service 'slapos-node-software-all' takes too long to finish"
)
# Get the supervisord environment
with
open
(
env_json_path
)
as
f
:
supervisord_env
=
json
.
load
(
f
)
# Compare relevant variables from both environments
self
.
maxDiff
=
None
self
.
assertEqual
(
theia_shell_env
[
'PATH'
].
split
(
':'
),
supervisord_env
[
'PATH'
].
split
(
':'
))
self
.
assertEqual
(
theia_shell_env
[
'SLAPOS_CONFIGURATION'
],
supervisord_env
[
'SLAPOS_CONFIGURATION'
])
self
.
assertEqual
(
theia_shell_env
[
'SLAPOS_CLIENT_CONFIGURATION'
],
supervisord_env
[
'SLAPOS_CLIENT_CONFIGURATION'
])
self
.
assertEqual
(
theia_shell_env
[
'HOME'
],
supervisord_env
[
'HOME'
])
finally
:
# Cleanup the theia shell process
theia_shell_process
.
terminate
()
theia_shell_process
.
wait
()
class
ResilientTheiaMixin
(
object
):
@
classmethod
def
setUpClass
(
cls
):
super
(
ResilientTheiaMixin
,
cls
).
setUpClass
()
# Add resiliency files to snapshot patterns
cls
.
_save_instance_file_pattern_list
+=
(
'*/srv/export-exitcode-file'
,
'*/srv/export-errormessage-file'
,
'*/srv/import-exitcode-file'
,
'*/srv/import-errormessage-file'
,
)
@
classmethod
def
_getPartition
(
cls
,
software_type
):
software_url
=
cls
.
getSoftwareURL
()
for
computer_partition
in
cls
.
slap
.
computer
.
getComputerPartitionList
():
partition_url
=
computer_partition
.
getSoftwareRelease
().
_software_release
partition_type
=
computer_partition
.
getType
()
if
partition_url
==
software_url
and
partition_type
==
software_type
:
return
computer_partition
raise
Exception
(
"Theia %s partition not found"
%
software_type
)
@
classmethod
def
_getPartitionId
(
cls
,
software_type
):
return
cls
.
_getPartition
(
software_type
).
getId
()
@
classmethod
def
_getPartitionPath
(
cls
,
software_type
,
*
paths
):
return
os
.
path
.
join
(
cls
.
slap
.
_instance_root
,
cls
.
_getPartitionId
(
software_type
),
*
paths
)
@
classmethod
def
_getSlapos
(
cls
,
software_type
=
'export'
):
return
cls
.
_getPartitionPath
(
software_type
,
'srv'
,
'runner'
,
'bin'
,
'slapos'
)
@
classmethod
def
getInstanceSoftwareType
(
cls
):
return
'resilient'
class
TestTheiaResilientInterface
(
ResilientTheiaMixin
,
TestTheia
):
@
classmethod
def
setUpClass
(
cls
):
super
(
TestTheiaResilientInterface
,
cls
).
setUpClass
()
# Patch the computer root path to that of the export theia instance
cls
.
computer_partition_root_path
=
cls
.
_getPartitionPath
(
'export'
)
class
TestTheiaResilientWithSR
(
ResilientTheiaMixin
,
TestTheiaWithSR
):
@
classmethod
def
setUpClass
(
cls
):
super
(
TestTheiaResilientWithSR
,
cls
).
setUpClass
()
# Patch the computer root path to that of the export theia instance
cls
.
computer_partition_root_path
=
cls
.
_getPartitionPath
(
'export'
)
software/theia/test/test_resiliency.py
0 → 100644
View file @
a271e8a8
##############################################################################
#
# Copyright (c) 2019 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from
__future__
import
unicode_literals
import
errno
import
os
import
re
import
shutil
import
six
import
subprocess
import
time
import
unittest
import
requests
from
slapos.testing.testcase
import
SlapOSNodeCommandError
,
installSoftwareUrlList
from
test
import
TheiaTestCase
,
ResilientTheiaMixin
,
theia_software_release_url
dummy_software_url
=
os
.
path
.
abspath
(
os
.
path
.
join
(
'resilience_dummy'
,
'software.cfg'
))
class
WorkaroundSnapshotConflict
(
TheiaTestCase
):
@
classmethod
def
_copySnapshot
(
cls
,
source_file_name
,
name
):
# Workaround setUpModule snapshots name conflicts
if
not
name
.
startswith
(
cls
.
__module__
):
name
=
'%s.%s'
%
(
cls
.
__module__
,
name
)
super
(
WorkaroundSnapshotConflict
,
cls
).
_copySnapshot
(
source_file_name
,
name
)
def
setUpModule
():
installSoftwareUrlList
(
WorkaroundSnapshotConflict
,
[
theia_software_release_url
],
debug
=
bool
(
int
(
os
.
environ
.
get
(
'SLAPOS_TEST_DEBUG'
,
0
))),
)
class
ResilientTheiaTestCase
(
ResilientTheiaMixin
,
TheiaTestCase
):
@
classmethod
def
_processEmbeddedInstance
(
cls
,
retries
=
0
,
software_type
=
'export'
):
slapos
=
cls
.
_getSlapos
(
software_type
)
for
_
in
range
(
retries
):
try
:
output
=
subprocess
.
check_output
((
slapos
,
'node'
,
'instance'
),
stderr
=
subprocess
.
STDOUT
)
except
subprocess
.
CalledProcessError
:
continue
print
(
output
)
break
else
:
if
retries
:
# Sleep a bit as an attempt to workaround monitoring boostrap not being ready
print
(
"Wait before running slapos node instance one last time"
)
time
.
sleep
(
120
)
subprocess
.
check_call
((
slapos
,
'node'
,
'instance'
))
@
classmethod
def
_deployEmbeddedSoftware
(
cls
,
software_url
,
instance_name
,
retries
=
0
,
software_type
=
'export'
):
slapos
=
cls
.
_getSlapos
(
software_type
)
subprocess
.
check_call
((
slapos
,
'supply'
,
software_url
,
'slaprunner'
))
try
:
subprocess
.
check_output
((
slapos
,
'node'
,
'software'
),
stderr
=
subprocess
.
STDOUT
)
except
subprocess
.
CalledProcessError
as
e
:
print
(
e
.
output
)
raise
subprocess
.
check_call
((
slapos
,
'request'
,
instance_name
,
software_url
))
cls
.
_processEmbeddedInstance
(
retries
,
software_type
)
@
classmethod
def
getInstanceParameterDict
(
cls
):
return
{
'autorun'
:
'stopped'
}
class
ResilienceMixin
(
object
):
def
_prepareExport
(
self
):
pass
def
_doSync
(
self
):
raise
NotImplementedError
def
_checkSync
(
self
):
pass
def
_doTakeover
(
self
):
raise
NotImplementedError
def
_checkTakeover
(
self
):
pass
def
test
(
self
):
# Do stuff on the main instance
# e.g. deploy an embedded software instance
self
.
_prepareExport
()
# Backup the main instance to a clone
# i.e. call export and import scripts
self
.
_doSync
()
# Check that the export-backup-import process went well
# e.g. look at logs and compare data
self
.
_checkSync
()
# Let the clone become a main instance
# i.e. start embedded services
self
.
_doTakeover
()
# Check that the takeover went well
# e.g. check services
self
.
_checkTakeover
()
class
ExportAndImportMixin
(
object
):
def
getExportExitfile
(
self
):
return
self
.
_getPartitionPath
(
'export'
,
'srv'
,
'export-exitcode-file'
)
def
getExportErrorfile
(
self
):
return
self
.
_getPartitionPath
(
'export'
,
'srv'
,
'export-errormessage-file'
)
def
getImportExitfile
(
self
):
return
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'import-exitcode-file'
)
def
getImportErrorfile
(
self
):
return
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'import-errormessage-file'
)
def
makedirs
(
self
,
path
):
try
:
os
.
makedirs
(
path
if
os
.
path
.
isdir
(
path
)
else
os
.
path
.
dirname
(
path
))
except
OSError
as
e
:
if
e
.
errno
!=
errno
.
EEXIST
:
raise
def
writeFile
(
self
,
path
,
content
,
mode
=
'w'
):
self
.
makedirs
(
path
)
executable
=
mode
==
'exec'
mode
=
'w'
if
executable
else
mode
with
open
(
path
,
mode
)
as
f
:
if
executable
:
f
.
write
(
'#!/bin/sh
\
n
'
)
f
.
write
(
content
)
if
executable
:
os
.
chmod
(
path
,
0o700
)
def
assertPromiseSucess
(
self
):
# Force promises to recompute regardless of periodicity
self
.
slap
.
_force_slapos_node_instance_all
=
True
try
:
self
.
slap
.
waitForInstance
(
error_lines
=
0
)
except
SlapOSNodeCommandError
as
e
:
s
=
str
(
e
)
self
.
assertNotIn
(
"Promise 'resiliency-export-promise.py' failed"
,
s
)
self
.
assertNotIn
(
'ERROR export script'
,
s
)
self
.
assertNotIn
(
"Promise 'resiliency-import-promise.py' failed"
,
s
)
self
.
assertNotIn
(
'ERROR import script'
,
s
)
else
:
pass
def
_doExport
(
self
):
# Compute last modification of the export exitcode file
exitfile
=
self
.
getExportExitfile
()
initial_exitdate
=
os
.
path
.
getmtime
(
exitfile
)
# Call export script manually
theia_export_script
=
self
.
_getPartitionPath
(
'export'
,
'bin'
,
'theia-export-script'
)
subprocess
.
check_call
((
theia_export_script
,),
stderr
=
subprocess
.
STDOUT
)
# Check that the export exitcode file was modified
self
.
assertGreater
(
os
.
path
.
getmtime
(
exitfile
),
initial_exitdate
)
with
open
(
exitfile
)
as
f
:
self
.
assertEqual
(
'0'
,
f
.
read
())
# Check promises
self
.
assertPromiseSucess
()
def
_doTransfer
(
self
):
# Copy <export>/srv/backup/theia to <import>/srv/backup/theia manually
export_backup_path
=
self
.
_getPartitionPath
(
'export'
,
'srv'
,
'backup'
,
'theia'
)
import_backup_path
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'backup'
,
'theia'
)
shutil
.
rmtree
(
import_backup_path
)
shutil
.
copytree
(
export_backup_path
,
import_backup_path
)
def
_doImport
(
self
):
# Compute last modification of the import exitcode file
exitfile
=
self
.
getImportExitfile
()
initial_exitdate
=
os
.
path
.
getmtime
(
exitfile
)
# Call the import script manually
theia_import_script
=
self
.
_getPartitionPath
(
'import'
,
'bin'
,
'theia-import-script'
)
subprocess
.
check_call
((
theia_import_script
,),
stderr
=
subprocess
.
STDOUT
)
# Check that the import exitcode file was updated
self
.
assertGreater
(
os
.
path
.
getmtime
(
exitfile
),
initial_exitdate
)
with
open
(
exitfile
)
as
f
:
self
.
assertEqual
(
'0'
,
f
.
read
())
# Check promises
self
.
assertPromiseSucess
()
class
TestTheiaExportAndImportFailures
(
ExportAndImportMixin
,
ResilientTheiaTestCase
):
script_relpath
=
os
.
path
.
join
(
'srv'
,
'runner'
,
'instance'
,
'slappart0'
,
'srv'
,
'.backup_identity_script'
)
def
assertPromiseFailure
(
self
,
*
msg
):
# Force promises to recompute regardless of periodicity
self
.
slap
.
_force_slapos_node_instance_all
=
True
try
:
self
.
slap
.
waitForInstance
(
error_lines
=
0
)
except
SlapOSNodeCommandError
as
e
:
s
=
str
(
e
).
replace
(
'
\
\
n'
,
'
\
n
'
)
for
m
in
msg
:
self
.
assertIn
(
m
,
s
)
else
:
self
.
fail
(
'No promise failed'
)
def
assertScriptFailure
(
self
,
func
,
errorfile
,
exitfile
,
*
msg
):
self
.
assertRaises
(
subprocess
.
CalledProcessError
,
func
,
)
if
msg
:
with
open
(
errorfile
)
as
f
:
error
=
f
.
read
()
for
m
in
msg
:
self
.
assertIn
(
m
,
error
)
with
open
(
exitfile
)
as
f
:
self
.
assertNotEqual
(
'0'
,
f
.
read
())
def
assertExportFailure
(
self
,
*
msg
):
self
.
assertScriptFailure
(
self
.
_doExport
,
self
.
getExportErrorfile
(),
self
.
getExportExitfile
(),
*
msg
)
self
.
assertPromiseFailure
(
'ERROR export script failed'
,
*
msg
)
def
assertImportFailure
(
self
,
*
msg
):
self
.
assertScriptFailure
(
self
.
_doImport
,
self
.
getImportErrorfile
(),
self
.
getImportExitfile
(),
*
msg
)
self
.
assertPromiseFailure
(
'ERROR import script failed'
,
*
msg
)
def
customScript
(
self
,
path
,
content
=
None
):
if
content
:
self
.
writeFile
(
path
,
content
,
mode
=
'exec'
)
else
:
if
os
.
path
.
exists
(
path
):
os
.
remove
(
path
)
def
customSignatureScript
(
self
,
content
=
None
):
custom_script
=
self
.
_getPartitionPath
(
'export'
,
self
.
script_relpath
)
self
.
customScript
(
custom_script
,
content
)
def
customRestoreScript
(
self
,
content
=
None
):
restore_script
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'runner-import-restore'
)
self
.
customScript
(
restore_script
,
content
)
return
restore_script
def
cleanupExitfiles
(
self
):
self
.
writeFile
(
self
.
getExportExitfile
(),
'0'
)
self
.
writeFile
(
self
.
getImportExitfile
(),
'0'
)
def
setUp
(
self
):
self
.
customSignatureScript
(
content
=
None
)
self
.
customRestoreScript
(
content
=
None
)
self
.
cleanupExitfiles
()
def
test_export_promise
(
self
):
self
.
writeFile
(
self
.
getExportExitfile
(),
'1'
)
self
.
assertPromiseFailure
(
'ERROR export script failed'
)
def
test_import_promise
(
self
):
self
.
writeFile
(
self
.
getImportExitfile
(),
'1'
)
self
.
assertPromiseFailure
(
'ERROR import script failed'
)
def
test_custom_hash_script
(
self
):
errmsg
=
'Bye bye'
self
.
customSignatureScript
(
content
=
'>&2 echo "%s"
\
n
exit 1'
%
errmsg
)
backup_script
=
self
.
_getPartitionPath
(
'export'
,
'srv'
,
'backup'
,
'theia'
,
self
.
script_relpath
)
self
.
assertExportFailure
(
'Compute backup signature
\
n
... ERROR !'
,
'Custom signature script %s failed'
%
os
.
path
.
abspath
(
backup_script
),
'and stderr:
\
n
%s'
%
errmsg
)
def
test_signature_mismatch
(
self
):
signature_file
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'backup'
,
'theia'
,
'backup.signature'
)
moved_file
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'backup'
,
'backup.signature.moved'
)
self
.
writeFile
(
moved_file
,
'Bogus Hash
\
n
'
,
mode
=
'a'
)
os
.
rename
(
moved_file
,
signature_file
)
self
.
assertImportFailure
(
'ERROR the backup signatures do not match'
)
def
test_restore_script_error
(
self
):
self
.
_doExport
()
self
.
_doTransfer
()
restore_script
=
self
.
customRestoreScript
(
'exit 1'
)
self
.
assertImportFailure
(
'Run custom restore script %s
\
n
... ERROR !'
%
restore_script
)
class
TestTheiaExportAndImport
(
ResilienceMixin
,
ExportAndImportMixin
,
ResilientTheiaTestCase
):
def
test_twice
(
self
):
# Run two synchronisations on the same instances
# to make sure everything still works the second time
self
.
_doSync
()
def
checkLog
(
self
,
log_path
,
initial
=
[],
newline
=
"Hello"
):
with
open
(
log_path
)
as
f
:
log
=
f
.
readlines
()
self
.
assertEqual
(
len
(
log
),
len
(
initial
)
+
int
(
bool
(
newline
)))
for
line
,
initial_line
in
zip
(
log
,
initial
):
self
.
assertEqual
(
line
,
initial_line
)
if
newline
:
self
.
assertTrue
(
log
[
-
1
].
startswith
(
newline
),
log
[
-
1
])
return
log
def
_prepareExport
(
self
):
# Copy ./resilience_dummy SR in export theia ~/srv/project/dummy
dummy_target_path
=
self
.
_getPartitionPath
(
'export'
,
'srv'
,
'project'
,
'dummy'
)
shutil
.
copytree
(
os
.
path
.
dirname
(
dummy_software_url
),
dummy_target_path
)
self
.
_test_software_url
=
os
.
path
.
join
(
dummy_target_path
,
'software.cfg'
)
# Deploy dummy instance in export partition
self
.
_deployEmbeddedSoftware
(
self
.
_test_software_url
,
'dummy_instance'
)
relpath_dummy
=
os
.
path
.
join
(
'srv'
,
'runner'
,
'instance'
,
'slappart0'
)
self
.
export_dummy_root
=
dummy_root
=
self
.
_getPartitionPath
(
'export'
,
relpath_dummy
)
self
.
import_dummy_root
=
self
.
_getPartitionPath
(
'import'
,
relpath_dummy
)
# Check that dummy instance was properly deployed
self
.
initial_log
=
self
.
checkLog
(
os
.
path
.
join
(
dummy_root
,
'log.log'
))
# Create ~/include and ~/include/included
self
.
writeFile
(
os
.
path
.
join
(
dummy_root
,
'include'
,
'included'
),
'This file should be included in resilient backup'
)
# Create ~/exclude and ~/exclude/excluded
self
.
writeFile
(
os
.
path
.
join
(
dummy_root
,
'exclude'
,
'excluded'
),
'This file should be excluded from resilient backup'
)
# Check that ~/srv/exporter.exclude and ~/srv/runner-import-restore
# As well as ~/srv/.backup_identity_script
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
dummy_root
,
'srv'
,
'exporter.exclude'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
dummy_root
,
'srv'
,
'runner-import-restore'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
dummy_root
,
'srv'
,
'.backup_identity_script'
)))
def
_doSync
(
self
):
self
.
_doExport
()
self
.
_doTransfer
()
self
.
_doImport
()
def
_checkSync
(
self
):
dummy_root
=
self
.
import_dummy_root
# Check that the software url is correct
adapted_test_url
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'project'
,
'dummy'
,
'software.cfg'
)
proxy_content
=
subprocess
.
check_output
(
(
self
.
_getSlapos
(
'import'
),
'proxy'
,
'show'
),
universal_newlines
=
True
)
self
.
assertIn
(
adapted_test_url
,
proxy_content
)
self
.
assertNotIn
(
self
.
_test_software_url
,
proxy_content
)
# Check that ~/srv/project was exported
self
.
assertTrue
(
os
.
path
.
exists
(
adapted_test_url
))
# Check that the dummy instance is not yet started
self
.
checkLog
(
os
.
path
.
join
(
dummy_root
,
'log.log'
),
self
.
initial_log
,
newline
=
None
)
# Check that ~/srv/.backup_identity_script was called
signature
=
self
.
_getPartitionPath
(
'import'
,
'srv'
,
'backup'
,
'backup.signature.proof'
)
with
open
(
signature
)
as
f
:
self
.
assertIn
(
'Custom script'
,
f
.
read
())
# Check that ~/include and ~/include/included were included
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
dummy_root
,
'include'
,
'included'
)))
# Check that ~/exclude was excluded
self
.
assertFalse
(
os
.
path
.
exists
(
os
.
path
.
join
(
dummy_root
,
'exclude'
)))
# Check that ~/srv/runner-import-restore was called
self
.
checkLog
(
os
.
path
.
join
(
dummy_root
,
'runner-import-restore.log'
))
def
_doTakeover
(
self
):
# Start the dummy instance as a sort of fake takeover
subprocess
.
check_call
((
self
.
_getSlapos
(
'import'
),
'node'
,
'instance'
))
def
_checkTakeover
(
self
):
# Check that dummy instance was properly re-deployed
log_path
=
os
.
path
.
join
(
self
.
import_dummy_root
,
'log.log'
)
self
.
checkLog
(
log_path
,
self
.
initial_log
)
class
TakeoverMixin
(
ExportAndImportMixin
):
def
_getTakeoverUrlAndPassword
(
self
,
scope
=
"theia-1"
):
parameter_dict
=
self
.
computer_partition
.
getConnectionParameterDict
()
takeover_url
=
parameter_dict
[
"takeover-%s-url"
%
scope
]
takeover_password
=
parameter_dict
[
"takeover-%s-password"
%
scope
]
return
takeover_url
,
takeover_password
def
_getTakeoverPage
(
self
,
takeover_url
):
resp
=
requests
.
get
(
takeover_url
,
verify
=
True
)
self
.
assertEqual
(
requests
.
codes
.
ok
,
resp
.
status_code
)
return
resp
.
text
def
_waitScriptDone
(
self
,
name
,
start
,
exitfile
,
errorfile
,
maxtries
,
interval
):
print
(
'Wait until %s script has run'
%
name
.
lower
())
for
t
in
range
(
maxtries
):
if
os
.
path
.
getmtime
(
exitfile
)
<
start
:
time
.
sleep
(
interval
)
continue
with
open
(
exitfile
)
as
f
:
if
f
.
read
()
==
'0'
:
print
(
name
+
' script ran successfully'
)
return
maxtries
-
t
print
(
name
+
' script failed:
\
n
'
)
with
open
(
errorfile
)
as
f
:
print
(
f
.
read
())
self
.
fail
(
name
+
' script failed'
)
self
.
fail
(
name
+
' script did not finish before timeout'
)
def
_waitTakeoverReady
(
self
,
takeover_url
,
start
,
maxtries
,
interval
):
export_exitfile
=
self
.
getExportExitfile
()
export_errorfile
=
self
.
getExportErrorfile
()
tries
=
self
.
_waitScriptDone
(
'Export'
,
start
,
export_exitfile
,
export_errorfile
,
maxtries
,
interval
)
import_exitfile
=
self
.
getImportExitfile
()
import_errorfile
=
self
.
getImportErrorfile
()
tries
=
self
.
_waitScriptDone
(
'Import'
,
start
,
import_exitfile
,
import_errorfile
,
tries
,
interval
)
for
_
in
range
(
tries
):
info
=
self
.
_getTakeoverPage
(
takeover_url
)
if
"No backup downloaded yet, takeover should not happen now."
in
info
:
print
(
'Takeover page still reports export script in progress'
)
elif
"<b>Importer script(s) of backup in progress:</b> True"
in
info
:
print
(
'Takeover page still reports import script in progress'
)
else
:
return
time
.
sleep
(
interval
)
self
.
fail
(
'Takeover page failed to report readiness'
)
def
_requestTakeover
(
self
,
takeover_url
,
takeover_password
):
resp
=
requests
.
get
(
"%s?password=%s"
%
(
takeover_url
,
takeover_password
),
verify
=
True
)
self
.
assertEqual
(
requests
.
codes
.
ok
,
resp
.
status_code
)
self
.
assertNotIn
(
"Error"
,
resp
.
text
,
"An Error occured: %s"
%
resp
.
text
)
self
.
assertIn
(
"Success"
,
resp
.
text
,
"An Error occured: %s"
%
resp
.
text
)
return
resp
.
text
@
unittest
.
skipIf
(
six
.
PY3
,
"resilient stack is not python3-compatible"
)
class
TestTheiaResilience
(
ResilienceMixin
,
TakeoverMixin
,
ResilientTheiaTestCase
):
test_instance_max_retries
=
0
backup_max_tries
=
70
backup_wait_interval
=
10
_test_software_url
=
dummy_software_url
def
_prepareExport
(
self
):
# Deploy test instance
self
.
_deployEmbeddedSoftware
(
self
.
_test_software_url
,
'test_instance'
,
self
.
test_instance_max_retries
)
# Check that there is an export and import instance and get their partition IDs
self
.
export_id
=
self
.
_getPartitionId
(
'export'
)
self
.
import_id
=
self
.
_getPartitionId
(
'import'
)
def
_doSync
(
self
):
start
=
time
.
time
()
# Call exporter script instead of waiting for cron job
# XXX Accelerate cron frequency instead ?
exporter_script
=
self
.
_getPartitionPath
(
'export'
,
'bin'
,
'exporter'
)
transaction_id
=
str
(
int
(
time
.
time
()))
subprocess
.
check_call
((
exporter_script
,
'--transaction-id'
,
transaction_id
))
takeover_url
,
_
=
self
.
_getTakeoverUrlAndPassword
()
# Wait for takoever to be ready
self
.
_waitTakeoverReady
(
takeover_url
,
start
,
self
.
backup_max_tries
,
self
.
backup_wait_interval
)
def
_doTakeover
(
self
):
# Takeover
takeover_url
,
takeover_password
=
self
.
_getTakeoverUrlAndPassword
()
self
.
_requestTakeover
(
takeover_url
,
takeover_password
)
# Wait for import instance to become export instance and new import to be allocated
# This also checks that all promises of theia instances succeed
self
.
slap
.
waitForInstance
(
self
.
instance_max_retry
)
self
.
computer_partition
=
self
.
requestDefaultInstance
()
def
_checkTakeover
(
self
):
# Check that there is an export, import and frozen instance and get their new partition IDs
import_id
=
self
.
import_id
export_id
=
self
.
export_id
new_export_id
=
self
.
_getPartitionId
(
'export'
)
new_import_id
=
self
.
_getPartitionId
(
'import'
)
new_frozen_id
=
self
.
_getPartitionId
(
'frozen'
)
# Check that old export instance is now frozen
self
.
assertEqual
(
export_id
,
new_frozen_id
)
# Check that old import instance is now the new export instance
self
.
assertEqual
(
import_id
,
new_export_id
)
# Check that there is a new import instance
self
.
assertNotIn
(
new_import_id
,
(
export_id
,
new_export_id
))
# Check that the test instance is properly redeployed
# This checks the promises of the test instance
self
.
_processEmbeddedInstance
(
self
.
test_instance_max_retries
)
software/theia/theia_common.py
0 → 100644
View file @
a271e8a8
import
contextlib
import
errno
import
glob
import
hashlib
import
os
import
re
import
subprocess
as
sp
import
sqlite3
import
six
import
zc.buildout.configparser
from
slapos.util
import
bytes2str
,
str2bytes
RSYNC_FLAGS
=
(
'-rlptgo'
,
'--safe-links'
,
'--stats'
,
'--ignore-missing-args'
,
'--delete'
,
'--delete-excluded'
)
RSYNC_REGEX
=
'^(file has vanished: |rsync warning: some files vanished before they could be transferred)'
EXCLUDE_PATTERNS
=
(
'*.sock'
,
'*.socket'
,
'*.pid'
,
'.installed*.cfg'
)
EXCLUDE_FLAGS
=
[
'--exclude={}'
.
format
(
x
)
for
x
in
sorted
(
EXCLUDE_PATTERNS
)]
def
makedirs
(
path
):
try
:
os
.
makedirs
(
path
if
os
.
path
.
isdir
(
path
)
else
os
.
path
.
dirname
(
path
))
except
OSError
as
e
:
if
e
.
errno
!=
errno
.
EEXIST
:
raise
def
copytree
(
rsyncbin
,
src
,
dst
,
exclude
=
[],
extrargs
=
[],
verbosity
=
'-v'
):
# Ensure there is a trailing slash in the source directory
# to avoid creating an additional directory level at the destination
src
=
os
.
path
.
join
(
src
,
''
)
# Compute absolute path of destination
dst
=
os
.
path
.
abspath
(
dst
)
# Create destination dir if it doesn't exist
makedirs
(
dst
)
command
=
[
rsyncbin
]
command
.
extend
(
RSYNC_FLAGS
)
# Exclude destination file from sources
command
.
append
(
'--filter=-/ {}'
.
format
(
dst
))
command
.
extend
(
EXCLUDE_FLAGS
)
command
.
extend
((
'--filter=-/ {}'
.
format
(
x
)
for
x
in
sorted
(
exclude
)))
command
.
extend
(
extrargs
)
command
.
append
(
verbosity
)
command
.
append
(
src
)
command
.
append
(
dst
)
try
:
return
sp
.
check_output
(
command
,
universal_newlines
=
True
)
except
sp
.
CalledProcessError
as
e
:
# Not all rsync errors are to be considered as errors
if
e
.
returncode
!=
24
or
re
.
search
(
RSYNC_REGEX
,
e
.
output
,
re
.
M
)
is
None
:
raise
return
e
.
output
def
copydb
(
sqlite3bin
,
src_db
,
dst_db
):
makedirs
(
dst_db
)
sp
.
check_output
((
sqlite3bin
,
src_db
,
'.backup '
+
dst_db
))
def
remove
(
path
):
try
:
os
.
remove
(
path
)
except
OSError
:
if
os
.
path
.
exists
(
path
):
raise
def
parse_installed
(
partition
):
paths
=
[]
custom_script
=
os
.
path
.
join
(
partition
,
'srv'
,
'.backup_identity_script'
)
for
cfg
in
glob
.
glob
(
os
.
path
.
join
(
partition
,
'.installed*.cfg'
)):
try
:
with
open
(
cfg
)
as
f
:
installed_cfg
=
zc
.
buildout
.
configparser
.
parse
(
f
,
cfg
)
except
IOError
as
e
:
if
e
.
errno
!=
errno
.
ENOENT
:
raise
else
:
for
section
in
six
.
itervalues
(
installed_cfg
):
for
p
in
section
.
get
(
'__buildout_installed__'
,
''
).
splitlines
():
p
=
p
.
strip
()
if
p
and
p
!=
custom_script
:
paths
.
append
(
p
)
return
paths
def
sha256sum
(
file_path
,
chunk_size
=
1024
*
1024
):
sha256
=
hashlib
.
sha256
()
with
open
(
file_path
,
'rb'
)
as
f
:
chunk
=
f
.
read
(
chunk_size
)
while
chunk
:
sha256
.
update
(
chunk
)
chunk
=
f
.
read
(
chunk_size
)
return
sha256
.
hexdigest
()
def
hashwalk
(
backup_dir
,
mirror_partitions
):
scripts
=
{}
for
p
in
mirror_partitions
:
script_path
=
os
.
path
.
join
(
p
,
'srv'
,
'.backup_identity_script'
)
if
os
.
path
.
exists
(
script_path
):
scripts
[
os
.
path
.
abspath
(
p
)]
=
script_path
for
dirpath
,
dirnames
,
filenames
in
os
.
walk
(
backup_dir
):
filenames
.
sort
()
for
f
in
filenames
:
filepath
=
os
.
path
.
join
(
dirpath
,
f
)
if
os
.
path
.
isfile
(
filepath
):
displaypath
=
os
.
path
.
relpath
(
filepath
,
start
=
backup_dir
)
yield
'%s %s'
%
(
sha256sum
(
filepath
),
displaypath
)
remaining_dirnames
=
[]
for
subdir
in
dirnames
:
subdirpath
=
os
.
path
.
abspath
(
os
.
path
.
join
(
dirpath
,
subdir
))
custom_hashscript
=
scripts
.
get
(
subdirpath
)
if
custom_hashscript
:
print
(
'Using custom signature script %s'
%
custom_hashscript
)
for
s
in
hashcustom
(
subdirpath
,
backup_dir
,
custom_hashscript
):
yield
s
else
:
remaining_dirnames
.
append
(
subdir
)
remaining_dirnames
.
sort
()
dirnames
[:]
=
remaining_dirnames
@
contextlib
.
contextmanager
def
cwd
(
path
):
old_path
=
os
.
getcwd
()
try
:
os
.
chdir
(
path
)
yield
finally
:
os
.
chdir
(
old_path
)
def
hashcustom
(
mirrordir
,
backup_dir
,
custom_hashscript
):
workingdir
=
os
.
path
.
join
(
mirrordir
,
os
.
pardir
,
os
.
pardir
,
os
.
pardir
)
with
cwd
(
os
.
path
.
abspath
(
workingdir
)):
for
dirpath
,
_
,
filenames
in
os
.
walk
(
mirrordir
):
filepaths
=
[]
for
f
in
filenames
:
path
=
os
.
path
.
join
(
dirpath
,
f
)
if
os
.
path
.
isfile
(
path
):
filepaths
.
append
(
'./'
+
os
.
path
.
relpath
(
path
,
start
=
workingdir
))
if
not
filepaths
:
continue
hashprocess
=
sp
.
Popen
(
custom_hashscript
,
stdin
=
sp
.
PIPE
,
stdout
=
sp
.
PIPE
,
stderr
=
sp
.
PIPE
)
out
,
err
=
hashprocess
.
communicate
(
str2bytes
(
'
\
0
'
.
join
(
filepaths
)))
if
hashprocess
.
returncode
!=
0
:
template
=
"Custom signature script %s failed on inputs:
\
n
%s"
msg
=
template
%
(
custom_hashscript
,
'
\
n
'
.
join
(
filepaths
))
msg
+=
"
\
n
with stdout:
\
n
%s"
%
bytes2str
(
out
)
msg
+=
"
\
n
and stderr:
\
n
%s"
%
bytes2str
(
err
)
raise
Exception
(
msg
)
signatures
=
bytes2str
(
out
).
strip
(
'
\
n
'
).
split
(
'
\
n
'
)
signatures
.
sort
()
displaypath
=
os
.
path
.
relpath
(
dirpath
,
start
=
backup_dir
)
for
s
in
signatures
:
yield
'%s %s/ (custom)'
%
(
s
,
displaypath
)
software/theia/theia_export.py
0 → 100644
View file @
a271e8a8
import
argparse
import
glob
import
itertools
import
os
import
sys
import
time
import
traceback
import
six
from
six.moves
import
configparser
sys
.
path
.
append
(
os
.
path
.
dirname
(
__file__
))
from
theia_common
import
copytree
,
copydb
,
hashwalk
,
parse_installed
,
remove
os
.
environ
[
'LC_ALL'
]
=
'C'
os
.
umask
(
0o77
)
BACKUP_WAIT
=
10
def
main
():
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
'--rsync'
,
required
=
True
)
parser
.
add_argument
(
'--sqlite3'
,
required
=
True
)
parser
.
add_argument
(
'--root'
,
required
=
True
)
parser
.
add_argument
(
'--backup'
,
required
=
True
)
parser
.
add_argument
(
'--cfg'
,
required
=
True
)
parser
.
add_argument
(
'--dirs'
,
action
=
'append'
)
parser
.
add_argument
(
'--exitfile'
,
required
=
True
)
parser
.
add_argument
(
'--errorfile'
,
required
=
True
)
args
=
parser
.
parse_args
()
TheiaExport
(
args
)()
class
TheiaExport
(
object
):
def
__init__
(
self
,
args
):
self
.
rsync_bin
=
args
.
rsync
self
.
sqlite3_bin
=
args
.
sqlite3
self
.
root_dir
=
args
.
root
self
.
backup_dir
=
args
.
backup
self
.
slapos_cfg
=
cfg
=
args
.
cfg
self
.
dirs
=
args
.
dirs
self
.
exit_file
=
args
.
exitfile
self
.
error_file
=
args
.
errorfile
configp
=
configparser
.
SafeConfigParser
()
configp
.
read
(
cfg
)
self
.
proxy_db
=
configp
.
get
(
'slapproxy'
,
'database_uri'
)
self
.
instance_dir
=
configp
.
get
(
'slapos'
,
'instance_root'
)
partitions
=
glob
.
glob
(
os
.
path
.
join
(
self
.
instance_dir
,
'slappart*'
))
self
.
partition_dirs
=
[
p
for
p
in
partitions
if
os
.
path
.
isdir
(
p
)]
self
.
copytree_partitions_args
=
{}
self
.
logs
=
[]
def
mirrorpath
(
self
,
src
):
return
os
.
path
.
abspath
(
os
.
path
.
join
(
self
.
backup_dir
,
os
.
path
.
relpath
(
src
,
start
=
self
.
root_dir
)))
def
backuptree
(
self
,
src
,
exclude
=
[],
extrargs
=
[],
verbosity
=
'-v'
):
dst
=
self
.
mirrorpath
(
src
)
return
copytree
(
self
.
rsync_bin
,
src
,
dst
,
exclude
,
extrargs
,
verbosity
)
def
backupdb
(
self
):
copydb
(
self
.
sqlite3_bin
,
self
.
proxy_db
,
self
.
mirrorpath
(
self
.
proxy_db
))
def
backuppartition
(
self
,
partition
):
installed
=
parse_installed
(
partition
)
rules
=
os
.
path
.
join
(
partition
,
'srv'
,
'exporter.exclude'
)
extrargs
=
(
'--filter=.-/ '
+
rules
,)
if
os
.
path
.
exists
(
rules
)
else
()
self
.
backuptree
(
partition
,
exclude
=
installed
,
extrargs
=
extrargs
)
self
.
copytree_partitions_args
[
partition
]
=
(
installed
,
extrargs
)
def
sign
(
self
,
signaturefile
):
remove
(
signaturefile
)
pardir
=
os
.
path
.
abspath
(
os
.
path
.
join
(
self
.
backup_dir
,
os
.
pardir
))
tmpfile
=
os
.
path
.
join
(
pardir
,
'backup.signature.tmp'
)
mirror_partitions
=
[
self
.
mirrorpath
(
p
)
for
p
in
self
.
partition_dirs
]
with
open
(
tmpfile
,
'w'
)
as
f
:
for
s
in
hashwalk
(
self
.
backup_dir
,
mirror_partitions
):
f
.
write
(
s
+
'
\
n
'
)
os
.
rename
(
tmpfile
,
signaturefile
)
def
checkpartition
(
self
,
partition
,
pattern
=
'/srv/backup/'
):
installed
,
extrargs
=
self
.
copytree_partitions_args
[
partition
]
output
=
self
.
backuptree
(
partition
,
exclude
=
installed
,
extrargs
=
extrargs
+
(
'--dry-run'
,
'--update'
),
verbosity
=
'--out-format=%n'
,
)
return
[
path
for
path
in
output
.
splitlines
()
if
pattern
in
path
]
def
loginfo
(
self
,
msg
):
print
(
msg
)
self
.
logs
.
append
(
msg
)
def
__call__
(
self
):
remove
(
self
.
error_file
)
exitcode
=
0
try
:
self
.
export
()
except
Exception
:
exitcode
=
1
exc
=
traceback
.
format_exc
()
with
open
(
self
.
error_file
,
'w'
)
as
f
:
f
.
write
(
'
\
n
... OK
\
n
\
n
'
.
join
(
self
.
logs
))
f
.
write
(
'
\
n
... ERROR !
\
n
\
n
'
)
f
.
write
(
exc
)
print
(
'
\
n
\
n
ERROR
\
n
\
n
'
+
exc
)
finally
:
with
open
(
self
.
exit_file
,
'w'
)
as
f
:
f
.
write
(
str
(
exitcode
))
sys
.
exit
(
exitcode
)
def
export
(
self
):
export_start_date
=
int
(
time
.
time
())
etc_dir
=
os
.
path
.
join
(
self
.
root_dir
,
'etc'
)
with
open
(
os
.
path
.
join
(
etc_dir
,
'.resilient_timestamp'
),
'w'
)
as
f
:
f
.
write
(
str
(
export_start_date
))
self
.
loginfo
(
'Backup directory '
+
etc_dir
)
self
.
backuptree
(
etc_dir
,
extrargs
=
(
'--filter=- */'
,
'--filter=-! .*'
))
for
d
in
self
.
dirs
:
self
.
loginfo
(
'Backup directory '
+
d
)
self
.
backuptree
(
d
)
self
.
loginfo
(
'Backup slapproxy database'
)
self
.
backupdb
()
self
.
loginfo
(
'Backup partitions'
)
for
p
in
self
.
partition_dirs
:
self
.
backuppartition
(
p
)
self
.
loginfo
(
'Compute backup signature'
)
self
.
sign
(
os
.
path
.
join
(
self
.
backup_dir
,
'backup.signature'
))
time
.
sleep
(
10
)
self
.
loginfo
(
'Check partitions'
)
modified
=
list
(
itertools
.
chain
.
from_iterable
(
self
.
checkpartition
(
p
)
for
p
in
self
.
partition_dirs
))
if
modified
:
msg
=
'Some files have been modified since the backup started'
self
.
loginfo
(
msg
+
':'
)
self
.
loginfo
(
'
\
n
'
.
join
(
modified
))
self
.
loginfo
(
"Let's wait %d minutes and try again"
%
BACKUP_WAIT
)
time
.
sleep
(
BACKUP_WAIT
*
60
)
raise
Exception
(
msg
)
self
.
loginfo
(
'Done'
)
if
__name__
==
'__main__'
:
main
()
software/theia/theia_import.py
0 → 100644
View file @
a271e8a8
import
argparse
import
glob
import
itertools
import
os
import
sys
import
subprocess
as
sp
import
traceback
import
six
from
six.moves
import
configparser
sys
.
path
.
append
(
os
.
path
.
dirname
(
__file__
))
from
theia_common
import
copytree
,
copydb
,
hashwalk
,
parse_installed
,
remove
os
.
environ
[
'LC_ALL'
]
=
'C'
os
.
umask
(
0o77
)
def
main
():
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
'--rsync'
,
required
=
True
)
parser
.
add_argument
(
'--sqlite3'
,
required
=
True
)
parser
.
add_argument
(
'--slapos'
,
required
=
True
)
parser
.
add_argument
(
'--srlog'
,
required
=
True
)
parser
.
add_argument
(
'--cplog'
,
required
=
True
)
parser
.
add_argument
(
'--supervisorctl'
,
required
=
True
)
parser
.
add_argument
(
'--supervisordconf'
,
required
=
True
)
parser
.
add_argument
(
'--root'
,
required
=
True
)
parser
.
add_argument
(
'--backup'
,
required
=
True
)
parser
.
add_argument
(
'--cfg'
,
required
=
True
)
parser
.
add_argument
(
'--dirs'
,
action
=
'append'
)
parser
.
add_argument
(
'--exitfile'
,
required
=
True
)
parser
.
add_argument
(
'--errorfile'
,
required
=
True
)
args
=
parser
.
parse_args
()
TheiaImport
(
args
)()
class
TheiaImport
(
object
):
def
__init__
(
self
,
args
):
self
.
rsync_bin
=
args
.
rsync
self
.
sqlite3_bin
=
args
.
sqlite3
self
.
slapos_bin
=
args
.
slapos
self
.
sr_log
=
args
.
srlog
self
.
cp_log
=
args
.
cplog
self
.
supervisorctl_bin
=
args
.
supervisorctl
self
.
supervisord_conf
=
args
.
supervisordconf
self
.
root_dir
=
args
.
root
self
.
backup_dir
=
args
.
backup
self
.
slapos_cfg
=
cfg
=
args
.
cfg
self
.
dirs
=
args
.
dirs
self
.
exit_file
=
args
.
exitfile
self
.
error_file
=
args
.
errorfile
configp
=
configparser
.
SafeConfigParser
()
configp
.
read
(
cfg
)
self
.
proxy_db
=
configp
.
get
(
'slapproxy'
,
'database_uri'
)
self
.
instance_dir
=
configp
.
get
(
'slapos'
,
'instance_root'
)
mirror_dir
=
self
.
mirrorpath
(
self
.
instance_dir
)
partitions
=
glob
.
glob
(
os
.
path
.
join
(
mirror_dir
,
'slappart*'
))
self
.
mirror_partition_dirs
=
[
p
for
p
in
partitions
if
os
.
path
.
isdir
(
p
)]
self
.
logs
=
[]
def
mirrorpath
(
self
,
dst
):
return
os
.
path
.
abspath
(
os
.
path
.
join
(
self
.
backup_dir
,
os
.
path
.
relpath
(
dst
,
start
=
self
.
root_dir
)))
def
dstpath
(
self
,
src
):
return
os
.
path
.
abspath
(
os
.
path
.
join
(
self
.
root_dir
,
os
.
path
.
relpath
(
src
,
start
=
self
.
backup_dir
)))
def
restoretree
(
self
,
dst
,
exclude
=
[],
extrargs
=
[],
verbosity
=
'-v'
):
src
=
self
.
mirrorpath
(
dst
)
return
copytree
(
self
.
rsync_bin
,
src
,
dst
,
exclude
,
extrargs
,
verbosity
)
def
restoredb
(
self
):
copydb
(
self
.
sqlite3_bin
,
self
.
mirrorpath
(
self
.
proxy_db
),
self
.
proxy_db
)
def
restorepartition
(
self
,
mirror_partition
):
p
=
self
.
dstpath
(
mirror_partition
)
installed
=
parse_installed
(
p
)
if
os
.
path
.
exists
(
p
)
else
[]
copytree
(
self
.
rsync_bin
,
mirror_partition
,
p
,
exclude
=
installed
)
def
supervisorctl
(
self
,
*
args
):
supervisor_command
=
(
self
.
supervisorctl_bin
,
'-c'
,
self
.
supervisord_conf
)
command
=
supervisor_command
+
args
print
(
' '
.
join
(
command
))
sp
.
check_call
(
command
)
def
slapos
(
self
,
*
args
):
command
=
(
self
.
slapos_bin
,)
+
args
+
(
'--cfg'
,
self
.
slapos_cfg
)
print
(
' '
.
join
(
command
))
sp
.
check_call
(
command
)
def
verify
(
self
,
signaturefile
):
pardir
=
os
.
path
.
abspath
(
os
.
path
.
join
(
self
.
backup_dir
,
os
.
pardir
))
moved
=
os
.
path
.
join
(
pardir
,
'backup.signature.moved'
)
proof
=
os
.
path
.
join
(
pardir
,
'backup.signature.proof'
)
if
os
.
path
.
exists
(
signaturefile
):
os
.
rename
(
signaturefile
,
moved
)
if
not
os
.
path
.
exists
(
moved
):
msg
=
'ERROR the backup signature file is missing'
print
(
msg
)
raise
Exception
(
msg
)
with
open
(
proof
,
'w'
)
as
f
:
for
s
in
hashwalk
(
self
.
backup_dir
,
self
.
mirror_partition_dirs
):
f
.
write
(
s
+
'
\
n
'
)
diffcommand
=
(
'diff'
,
moved
,
proof
)
print
(
' '
.
join
(
diffcommand
))
try
:
sp
.
check_output
(
diffcommand
,
stderr
=
sp
.
STDOUT
,
universal_newlines
=
True
)
except
sp
.
CalledProcessError
as
e
:
template
=
'ERROR the backup signatures do not match
\
n
\
n
%s'
msg
=
template
%
e
.
output
print
(
msg
)
raise
Exception
(
msg
)
def
loginfo
(
self
,
msg
):
print
(
msg
)
self
.
logs
.
append
(
msg
)
def
__call__
(
self
):
remove
(
self
.
error_file
)
exitcode
=
0
try
:
self
.
restore
()
except
Exception
:
exitcode
=
1
exc
=
traceback
.
format_exc
()
with
open
(
self
.
error_file
,
'w'
)
as
f
:
f
.
write
(
'
\
n
... OK
\
n
\
n
'
.
join
(
self
.
logs
))
f
.
write
(
'
\
n
... ERROR !
\
n
\
n
'
)
f
.
write
(
exc
)
print
(
'
\
n
\
n
ERROR
\
n
\
n
'
+
exc
)
finally
:
with
open
(
self
.
exit_file
,
'w'
)
as
f
:
f
.
write
(
str
(
exitcode
))
sys
.
exit
(
exitcode
)
def
restore
(
self
):
self
.
loginfo
(
'Verify backup signature'
)
self
.
verify
(
os
.
path
.
join
(
self
.
backup_dir
,
'backup.signature'
))
self
.
loginfo
(
'Stop slapproxy'
)
self
.
supervisorctl
(
'stop'
,
'slapos-proxy'
)
self
.
loginfo
(
'Restore partitions'
)
for
m
in
self
.
mirror_partition_dirs
:
self
.
restorepartition
(
m
)
for
d
in
self
.
dirs
:
self
.
loginfo
(
'Restore directory '
+
d
)
self
.
restoretree
(
d
)
self
.
loginfo
(
'Restore slapproxy database'
)
self
.
restoredb
()
etc_dir
=
os
.
path
.
join
(
self
.
root_dir
,
'etc'
)
self
.
loginfo
(
'Restore directory '
+
etc_dir
)
self
.
restoretree
(
etc_dir
,
extrargs
=
(
'--filter=- */'
,
'--filter=-! .*'
))
custom_script
=
os
.
path
.
join
(
self
.
root_dir
,
'srv'
,
'runner-import-restore'
)
if
os
.
path
.
exists
(
custom_script
):
self
.
loginfo
(
'Run custom restore script %s'
%
custom_script
)
sp
.
check_call
(
custom_script
)
self
.
loginfo
(
'Start slapproxy again'
)
self
.
supervisorctl
(
'start'
,
'slapos-proxy'
)
self
.
loginfo
(
'Reformat partitions'
)
self
.
slapos
(
'node'
,
'format'
,
'--now'
)
self
.
loginfo
(
'Remove old supervisord configuration files'
)
conf_dir
=
os
.
path
.
join
(
self
.
instance_dir
,
'etc'
,
'supervisor.conf.d'
)
for
f
in
glob
.
glob
(
os
.
path
.
join
(
conf_dir
,
'*'
)):
os
.
remove
(
f
)
self
.
loginfo
(
'Build Software Releases'
)
for
i
in
range
(
3
):
try
:
self
.
slapos
(
'node'
,
'software'
,
'--all'
,
'--logfile'
,
self
.
sr_log
)
except
sp
.
CalledProcessError
:
if
i
==
2
:
raise
else
:
break
self
.
loginfo
(
'Remove old custom instance scripts'
)
partitions_glob
=
os
.
path
.
join
(
self
.
instance_dir
,
'slappart*'
)
scripts
=
os
.
path
.
join
(
partitions_glob
,
'srv'
,
'runner-import-restore'
)
for
f
in
glob
.
glob
(
scripts
):
remove
(
f
)
self
.
loginfo
(
'Remove partition timestamps'
)
timestamps
=
os
.
path
.
join
(
partitions_glob
,
'.timestamp'
)
for
f
in
glob
.
glob
(
timestamps
):
remove
(
f
)
self
.
loginfo
(
'Build Instances'
)
cp_log
=
self
.
cp_log
for
i
in
range
(
3
):
try
:
self
.
slapos
(
'node'
,
'instance'
,
'--force-stop'
,
'--logfile'
,
cp_log
)
except
sp
.
CalledProcessError
:
if
i
==
2
:
raise
else
:
break
for
custom_script
in
glob
.
glob
(
scripts
):
self
.
loginfo
(
'Running custom instance script %s'
%
custom_script
)
sp
.
check_call
(
custom_script
)
self
.
loginfo
(
'Done'
)
if
__name__
==
'__main__'
:
main
()
stack/resilient/buildout.hash.cfg
View file @
a271e8a8
...
...
@@ -14,7 +14,7 @@
# not need these here).
[pbsready]
filename = pbsready.cfg.in
md5sum =
9ceceeee21fa90837c887d2d6866859e
md5sum =
005125621d157b3ae04c428ea6060e37
[pbsready-import]
filename = pbsready-import.cfg.in
...
...
@@ -26,7 +26,7 @@ md5sum = 2b0c71b085cfe8017f28098c160b1f49
[template-pull-backup]
filename = instance-pull-backup.cfg.in
md5sum =
e64e13854332bcc2595df187fcae1203
md5sum =
b240dc76a663190304d8bcb9cabcda8f
[template-replicated]
filename = template-replicated.cfg.in
...
...
stack/resilient/instance-pull-backup.cfg.in
View file @
a271e8a8
...
...
@@ -137,12 +137,17 @@ command-line = ${buildout:bin-directory}/generatefeed --output $${:feed-path} --
feed-path = $${directory:monitor-resilient}/pbs-status-rss
wrapper-path = $${rootdirectory:bin}/resilient-genstatusrss.py
[pbs-status-feed-first-run]
recipe = plone.recipe.command
command = $${pbs-resilient-status-feed:wrapper-path}
stop-on-error = true
[cron-pbs-status-feed]
<= cron
recipe = slapos.cookbook:cron.d
name = resilient-pbs-status-feed
frequency = */5 * * * *
command = $${pbs-
resilient-status-feed:wrapper-path
}
command = $${pbs-
status-feed-first-run:command
}
[logrotate-entry-notifier]
<= logrotate-entry-base
...
...
stack/resilient/pbsready.cfg.in
View file @
a271e8a8
...
...
@@ -145,12 +145,17 @@ command-line = ${buildout:bin-directory}/generatefeed --output $${:feed-path} --
feed-path = $${directory:monitor-resilient}/notifier-status-rss
wrapper-path = $${rootdirectory:bin}/resilient-genstatusrss.py
[notifier-status-feed-first-run]
recipe = plone.recipe.command
command = $${notifier-resilient-status-feed:wrapper-path}
stop-on-error = true
[cron-entry-notifier-status-feed]
<= cron
recipe = slapos.cookbook:cron.d
name = resilient-notifier-status-feed
frequency = */5 * * * *
command = $${notifier-
resilient-status-feed:wrapper-path
}
command = $${notifier-
status-feed-first-run:command
}
[notifier-stalled-promise-bin]
recipe = slapos.cookbook:wrapper
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment