gnuhealth-control 5.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gnuhealth-control
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: GNU Health HIS installer and instance administration tool
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Keywords: health,hospital,HIS,eHealth,gnuhealth
|
|
7
|
+
Author: GNU Solidario
|
|
8
|
+
Author-email: health@gnusolidario.org
|
|
9
|
+
Maintainer: Luis Falcon
|
|
10
|
+
Maintainer-email: falcon@gnuhealth.org
|
|
11
|
+
Requires-Python: >=3.10,<3.14
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console :: Curses
|
|
14
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
26
|
+
Requires-Dist: proteus (>=7.0.0,<7.1)
|
|
27
|
+
Requires-Dist: psutil (>=7.0.0,<8.0.0)
|
|
28
|
+
Project-URL: Bug Tracker, https://codeberg.org/gnuhealth/his-utils/issues
|
|
29
|
+
Project-URL: Documentation, https://docs.gnuhealth.org
|
|
30
|
+
Project-URL: Homepage, https://www.gnuhealth.org
|
|
31
|
+
Project-URL: Repository, https://codeberg.org/gnuhealth/his-utils
|
|
32
|
+
Description-Content-Type: text/x-rst
|
|
33
|
+
|
|
34
|
+
.. SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
|
|
35
|
+
.. SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>
|
|
36
|
+
..
|
|
37
|
+
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
|
38
|
+
|
|
39
|
+
The GNU Health HIS Installer and Manager
|
|
40
|
+
========================================
|
|
41
|
+
|
|
42
|
+
The gnuhealth-control is a single-entry point to install, setup and manage
|
|
43
|
+
the GNU Health Hospital Management System
|
|
44
|
+
|
|
45
|
+
Installation
|
|
46
|
+
------------
|
|
47
|
+
GNU Health Control application can be installed with pip::
|
|
48
|
+
|
|
49
|
+
$ pip install --upgrade gnuhealth-control
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Technology
|
|
53
|
+
----------
|
|
54
|
+
The GNU Health Control is a self-contained curses Python application.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Development
|
|
58
|
+
-----------
|
|
59
|
+
The development of gnuhealth-control will be done at Codeberg.
|
|
60
|
+
|
|
61
|
+
The development mailing list is at health-dev@gnu.org
|
|
62
|
+
General questions can be done on health@gnu.org mailing list.
|
|
63
|
+
|
|
64
|
+
Note: You need to subscribe before posting or the messages will be automatically
|
|
65
|
+
discarded. More at: https://docs.gnuhealth.org/his/support.html
|
|
66
|
+
|
|
67
|
+
Homepage
|
|
68
|
+
--------
|
|
69
|
+
https://www.gnuhealth.org
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Documentation
|
|
73
|
+
-------------
|
|
74
|
+
|
|
75
|
+
https://docs.gnuhealth.org
|
|
76
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.. SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
|
|
2
|
+
.. SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>
|
|
3
|
+
..
|
|
4
|
+
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
|
|
6
|
+
The GNU Health HIS Installer and Manager
|
|
7
|
+
========================================
|
|
8
|
+
|
|
9
|
+
The gnuhealth-control is a single-entry point to install, setup and manage
|
|
10
|
+
the GNU Health Hospital Management System
|
|
11
|
+
|
|
12
|
+
Installation
|
|
13
|
+
------------
|
|
14
|
+
GNU Health Control application can be installed with pip::
|
|
15
|
+
|
|
16
|
+
$ pip install --upgrade gnuhealth-control
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Technology
|
|
20
|
+
----------
|
|
21
|
+
The GNU Health Control is a self-contained curses Python application.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Development
|
|
25
|
+
-----------
|
|
26
|
+
The development of gnuhealth-control will be done at Codeberg.
|
|
27
|
+
|
|
28
|
+
The development mailing list is at health-dev@gnu.org
|
|
29
|
+
General questions can be done on health@gnu.org mailing list.
|
|
30
|
+
|
|
31
|
+
Note: You need to subscribe before posting or the messages will be automatically
|
|
32
|
+
discarded. More at: https://docs.gnuhealth.org/his/support.html
|
|
33
|
+
|
|
34
|
+
Homepage
|
|
35
|
+
--------
|
|
36
|
+
https://www.gnuhealth.org
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Documentation
|
|
40
|
+
-------------
|
|
41
|
+
|
|
42
|
+
https://docs.gnuhealth.org
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 GNU Solidario <health@gnusolidario.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
[build-system]
|
|
6
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
7
|
+
build-backend = "poetry.core.masonry.api"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "gnuhealth-control"
|
|
11
|
+
version = "5.0.0"
|
|
12
|
+
description = "GNU Health HIS installer and instance administration tool"
|
|
13
|
+
license = { text = "GPL-3.0-or-later" }
|
|
14
|
+
readme = "README.rst"
|
|
15
|
+
keywords = ["health", "hospital", "HIS", "eHealth", "gnuhealth"]
|
|
16
|
+
requires-python = ">=3.10,<3.14"
|
|
17
|
+
authors = [
|
|
18
|
+
{name = "GNU Solidario", email = "health@gnusolidario.org"},
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dynamic = [ "classifiers" ]
|
|
22
|
+
|
|
23
|
+
maintainers = [
|
|
24
|
+
{ name = "Luis Falcon", email = "falcon@gnuhealth.org" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"psutil (>=7.0.0,<8.0.0)",
|
|
29
|
+
"proteus (>=7.0.0,<7.1)",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
homepage = "https://www.gnuhealth.org"
|
|
35
|
+
repository = "https://codeberg.org/gnuhealth/his-utils"
|
|
36
|
+
documentation = "https://docs.gnuhealth.org"
|
|
37
|
+
"Bug Tracker" = "https://codeberg.org/gnuhealth/his-utils/issues"
|
|
38
|
+
|
|
39
|
+
[tool.poetry]
|
|
40
|
+
classifiers=[
|
|
41
|
+
'Development Status :: 5 - Production/Stable',
|
|
42
|
+
'Environment :: Console :: Curses',
|
|
43
|
+
'Intended Audience :: System Administrators',
|
|
44
|
+
'Intended Audience :: Healthcare Industry',
|
|
45
|
+
'Natural Language :: English',
|
|
46
|
+
'Operating System :: OS Independent',
|
|
47
|
+
'Programming Language :: Python :: 3',
|
|
48
|
+
'Topic :: Scientific/Engineering :: Bio-Informatics',
|
|
49
|
+
'Topic :: Scientific/Engineering :: Medical Science Apps.',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
ghcontrol = 'gnuhealth_control.ghcontrol:main'
|
|
File without changes
|
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org> #
|
|
2
|
+
# SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org> #
|
|
3
|
+
# #
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later #
|
|
5
|
+
#############################################################################
|
|
6
|
+
# Hospital Management Information System (HMIS) component of the #
|
|
7
|
+
# GNU Health project #
|
|
8
|
+
# https://www.gnuhealth.org #
|
|
9
|
+
#############################################################################
|
|
10
|
+
# ghcontrol.py #
|
|
11
|
+
#############################################################################
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from curses import wrapper, panel
|
|
15
|
+
from venv import EnvBuilder
|
|
16
|
+
from shutil import copy
|
|
17
|
+
from importlib.metadata import distributions
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import curses
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import psutil
|
|
25
|
+
import string
|
|
26
|
+
import random
|
|
27
|
+
|
|
28
|
+
# ghcontrol version shares release major and minor numbers with GH HIS.
|
|
29
|
+
VERSION = "5.0.0" # gnuheath-control version
|
|
30
|
+
MIN_VER = '>=5.0.0a1,<5.1' # Initial gnuhealth server package version
|
|
31
|
+
TRYTON_PINNING = '>=7.0,<7.1' # Pin the tryton packages to this range
|
|
32
|
+
|
|
33
|
+
""" GNU Health control limits the health packages to the official ones
|
|
34
|
+
for a particular release
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
HEALTH_PACKAGES = [
|
|
38
|
+
"archives", "caldav", "calendar", "contact-tracing", "crypto",
|
|
39
|
+
"crypto-lab", "dentistry", "disability", "ems", "federation", "genetics",
|
|
40
|
+
"genetics-uniprot", "gyneco", "history", "icd10", "icd10pcs", "icd11",
|
|
41
|
+
"icd9procs", "icpm", "icu", "imaging", "imaging-worklist", "inpatient",
|
|
42
|
+
"inpatient-calendar", "insurance", "iss", "lab", "lifestyle", "mdg6",
|
|
43
|
+
"ntd", "ntd-chagas", "ntd-dengue", "nursing", "ophthalmology", "orthanc",
|
|
44
|
+
"pediatrics", "pediatrics-growth-charts", "pediatrics-growth-charts-who",
|
|
45
|
+
"qrcodes", "reporting", "services", "services-imaging", "services-lab",
|
|
46
|
+
"socioeconomics", "stock", "stock-inpatient", "stock-nursing",
|
|
47
|
+
"stock-surgery", "surgery", "surgery-protocols", "webdav3-server",
|
|
48
|
+
"who-essential-medicines"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def log_window():
|
|
52
|
+
winlen = 45
|
|
53
|
+
x = int(curses.COLS/2 - winlen/2)
|
|
54
|
+
logwin = curses.newwin(10, winlen, 2, x)
|
|
55
|
+
logwin.box()
|
|
56
|
+
curses.echo()
|
|
57
|
+
return logwin
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def show_logs(win, row, col, data, attr=None):
|
|
61
|
+
win.addstr(row, col, data, attr)
|
|
62
|
+
win.refresh()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def task_engine(task, window, row):
|
|
66
|
+
task_name = task[0][0]
|
|
67
|
+
args = task[0][1]
|
|
68
|
+
tsk = task[1]
|
|
69
|
+
|
|
70
|
+
task_str = f"Running {task_name} ..."
|
|
71
|
+
show_logs(win=window, row=row, col=1, data=task_str, attr=0)
|
|
72
|
+
window.refresh()
|
|
73
|
+
|
|
74
|
+
rc = tsk(args)
|
|
75
|
+
if rc == 0:
|
|
76
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
77
|
+
result = "[OK]"
|
|
78
|
+
else:
|
|
79
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
80
|
+
result = "[ERROR]"
|
|
81
|
+
col = 35
|
|
82
|
+
show_logs(win=window, row=row, col=col, data=result, attr=attr)
|
|
83
|
+
window.refresh()
|
|
84
|
+
return rc
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_timestamp():
|
|
88
|
+
return datetime.now().strftime("%Y%m%d%H%M%S")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Server:
|
|
92
|
+
|
|
93
|
+
arguments = None
|
|
94
|
+
|
|
95
|
+
def __init__(self, stdscr, arguments):
|
|
96
|
+
self.arguments = arguments
|
|
97
|
+
self.stdscr = stdscr
|
|
98
|
+
|
|
99
|
+
def get_base_dir(self):
|
|
100
|
+
basedir = self.arguments.basedir
|
|
101
|
+
return basedir
|
|
102
|
+
|
|
103
|
+
def get_his_dir(self):
|
|
104
|
+
basedir = self.get_base_dir()
|
|
105
|
+
major_minor = re.match(r'(^[0-9]+)\.([0-9]+)\.(.+$)', VERSION)
|
|
106
|
+
rel = f"{major_minor[1]}{major_minor[2]}"
|
|
107
|
+
release = self.arguments.release or rel
|
|
108
|
+
his_dir = f"{basedir}/his-{release}"
|
|
109
|
+
return his_dir
|
|
110
|
+
|
|
111
|
+
def get_pip_cache_dir(self):
|
|
112
|
+
basedir = self.get_base_dir()
|
|
113
|
+
pip_cache_dir = f"{basedir}/cache/pip"
|
|
114
|
+
return pip_cache_dir
|
|
115
|
+
|
|
116
|
+
def setup_dirs(self):
|
|
117
|
+
""" Creates the basic directory structure to hold GH HIS
|
|
118
|
+
components
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
basedir = self.get_base_dir()
|
|
122
|
+
his_dir = self.get_his_dir()
|
|
123
|
+
pip_cache_dir = self.get_pip_cache_dir()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Create root directory
|
|
127
|
+
os.makedirs(basedir)
|
|
128
|
+
|
|
129
|
+
# Create pip cache directory
|
|
130
|
+
os.makedirs(pip_cache_dir, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
except FileExistsError: # Allow the basedir to exist
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Create his directory
|
|
137
|
+
os.mkdir(his_dir)
|
|
138
|
+
|
|
139
|
+
except BaseException:
|
|
140
|
+
return -1
|
|
141
|
+
|
|
142
|
+
for subdir in ["etc", "log", "local", "attach", "backup"]:
|
|
143
|
+
try:
|
|
144
|
+
# Create subdirs
|
|
145
|
+
os.mkdir(f"{his_dir}/{subdir}")
|
|
146
|
+
except BaseException:
|
|
147
|
+
return -1
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
def setup_virtualenv(self):
|
|
152
|
+
""" Create the virtual environment
|
|
153
|
+
"""
|
|
154
|
+
his_dir = self.get_his_dir()
|
|
155
|
+
venvdir = f"{his_dir}/venv"
|
|
156
|
+
ghvenv = EnvBuilder(system_site_packages=False, with_pip=True)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
ghvenv.create(env_dir=venvdir)
|
|
160
|
+
except BaseException:
|
|
161
|
+
return -1
|
|
162
|
+
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
def install_core(self):
|
|
166
|
+
""" Install the core package ("gnuhealth")
|
|
167
|
+
in the newly created virtual environment
|
|
168
|
+
"""
|
|
169
|
+
his_dir = self.get_his_dir()
|
|
170
|
+
pip_cache_dir = self.get_pip_cache_dir()
|
|
171
|
+
npython = f"{his_dir}/venv/bin/python"
|
|
172
|
+
|
|
173
|
+
""" The following vars will only be used in development
|
|
174
|
+
"""
|
|
175
|
+
pre = self.arguments.pre
|
|
176
|
+
test_repo = "https://test.pypi.org/simple/"
|
|
177
|
+
extra_index = "https://pypi.org/simple/"
|
|
178
|
+
deplog = f"{his_dir}/log/install_deps.log"
|
|
179
|
+
with open(deplog, "w") as logfile:
|
|
180
|
+
if (self.arguments.test):
|
|
181
|
+
process = [
|
|
182
|
+
npython, '-m', 'pip', 'install', '-i', test_repo,
|
|
183
|
+
'--extra-index-url', extra_index,
|
|
184
|
+
'--cache-dir', pip_cache_dir,
|
|
185
|
+
'--upgrade', f"gnuhealth{MIN_VER}"]
|
|
186
|
+
else:
|
|
187
|
+
process = [
|
|
188
|
+
npython, '-m', 'pip', 'install', '--cache-dir',
|
|
189
|
+
pip_cache_dir, '--upgrade',
|
|
190
|
+
f"gnuhealth{MIN_VER}"]
|
|
191
|
+
|
|
192
|
+
if pre:
|
|
193
|
+
process.append('--pre') # Append pip pre-release argument
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
subprocess.run(process, stdout=logfile, stderr=logfile)
|
|
197
|
+
|
|
198
|
+
except BaseException:
|
|
199
|
+
return -1
|
|
200
|
+
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
def check_config_dir(self):
|
|
204
|
+
his_dir = self.get_his_dir()
|
|
205
|
+
etc_dir = f"{his_dir}/etc"
|
|
206
|
+
|
|
207
|
+
if os.path.exists(etc_dir) and os.path.isdir(etc_dir):
|
|
208
|
+
return 0
|
|
209
|
+
else:
|
|
210
|
+
return -1
|
|
211
|
+
|
|
212
|
+
def gnuhealthrc(self):
|
|
213
|
+
""" Generate the gnuhealthrc profile.
|
|
214
|
+
We explicitly set the virtual environment (VIRTUAL_ENV)
|
|
215
|
+
and path so no need to source 'activate' script
|
|
216
|
+
Set aliases and editor for interactive sessions
|
|
217
|
+
For non-interactive sessions there are also the following
|
|
218
|
+
scripts:
|
|
219
|
+
* start_gnuhealth
|
|
220
|
+
* editconf
|
|
221
|
+
* ghis_env
|
|
222
|
+
"""
|
|
223
|
+
his_dir = self.get_his_dir()
|
|
224
|
+
venvdir = f"{his_dir}/venv"
|
|
225
|
+
trytond_cfg = f"{his_dir}/etc/trytond.conf"
|
|
226
|
+
trytond_log_conf = f"{his_dir}/etc/server_log.conf"
|
|
227
|
+
|
|
228
|
+
ghrc = f'{his_dir}/etc/gnuhealthrc'
|
|
229
|
+
self.create_bash_wrap(
|
|
230
|
+
file_name="etc/gnuhealthrc",
|
|
231
|
+
executable=False,
|
|
232
|
+
need_backup=True,
|
|
233
|
+
file_content=f"""\
|
|
234
|
+
export VIRTUAL_ENV={venvdir}
|
|
235
|
+
export PATH={his_dir}:{venvdir}/bin:$PATH
|
|
236
|
+
export TRYTOND_CONFIG={trytond_cfg}
|
|
237
|
+
export TRYTOND_LOGGING_CONFIG={trytond_log_conf}
|
|
238
|
+
export GNUHEALTH_HIS_BASE={his_dir}
|
|
239
|
+
|
|
240
|
+
alias cdlogs='cd {his_dir}/log'
|
|
241
|
+
alias cdbase='cd {his_dir}'
|
|
242
|
+
|
|
243
|
+
# Avoid accidental execution of rm, mv or cp
|
|
244
|
+
alias | grep rm= &> /dev/null || alias rm='rm -i'
|
|
245
|
+
alias | grep mv= &> /dev/null || alias mv='mv -i'
|
|
246
|
+
alias | grep cp= &> /dev/null || alias cp='cp -i'
|
|
247
|
+
""")
|
|
248
|
+
|
|
249
|
+
ghenv = f'{his_dir}/ghis_env'
|
|
250
|
+
self.create_bash_wrap("ghis_env", f"""\
|
|
251
|
+
#!/usr/bin/env bash
|
|
252
|
+
|
|
253
|
+
[[ -f {venvdir}/bin/activate ]] && source {venvdir}/bin/activate
|
|
254
|
+
[[ -f {ghrc} ]] && source {ghrc}
|
|
255
|
+
|
|
256
|
+
export GNUHEALTH_ENV=1
|
|
257
|
+
|
|
258
|
+
exec \"$@\"
|
|
259
|
+
""")
|
|
260
|
+
self.create_bash_wrap("start_gnuhealth", f"""\
|
|
261
|
+
#!/usr/bin/env bash
|
|
262
|
+
|
|
263
|
+
{ghenv} trytond \"$@\"
|
|
264
|
+
""")
|
|
265
|
+
|
|
266
|
+
self.create_bash_wrap("editconf", f"""\
|
|
267
|
+
#!/usr/bin/env bash
|
|
268
|
+
|
|
269
|
+
if command -v nano &> /dev/null; then
|
|
270
|
+
export EDITOR=nano
|
|
271
|
+
else
|
|
272
|
+
export EDITOR=vi
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
$EDITOR {trytond_cfg} \"$@\"
|
|
276
|
+
""")
|
|
277
|
+
|
|
278
|
+
self.modify_bashrc(ghrc)
|
|
279
|
+
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
def create_bash_wrap(self, file_name, file_content,
|
|
283
|
+
executable=True,
|
|
284
|
+
need_backup=False):
|
|
285
|
+
his_dir = self.get_his_dir()
|
|
286
|
+
file_path = f'{his_dir}/{file_name}'
|
|
287
|
+
timestamp = self.get_timestamp()
|
|
288
|
+
|
|
289
|
+
if need_backup:
|
|
290
|
+
file_backup = f"{file_path}.back-{timestamp}"
|
|
291
|
+
if os.path.isfile(file_path):
|
|
292
|
+
copy(file_path, file_backup)
|
|
293
|
+
|
|
294
|
+
with open(file_path, "w") as f:
|
|
295
|
+
file_content = file_content
|
|
296
|
+
f.write(file_content)
|
|
297
|
+
if executable:
|
|
298
|
+
os.chmod(file_path, 0o755)
|
|
299
|
+
|
|
300
|
+
def get_timestamp(self):
|
|
301
|
+
return datetime.now().strftime("%Y%m%d%H%M%S")
|
|
302
|
+
|
|
303
|
+
def modify_bashrc(self, ghrc):
|
|
304
|
+
home = os.environ['HOME']
|
|
305
|
+
bashrc = f"{home}/.bashrc"
|
|
306
|
+
timestamp = self.get_timestamp()
|
|
307
|
+
bashrc_back = f"{home}/.bashrc.back-{timestamp}"
|
|
308
|
+
|
|
309
|
+
if not self.arguments.ignore_bashrc:
|
|
310
|
+
if os.path.isfile(bashrc):
|
|
311
|
+
copy(bashrc, bashrc_back) # Make a backup of bashrc
|
|
312
|
+
self.modify_bashrc_gnuhealth_section(
|
|
313
|
+
bashrc, f"[[ -f {ghrc} ]] && source {ghrc}")
|
|
314
|
+
|
|
315
|
+
# --update-config will also update $HOME/.bashrc
|
|
316
|
+
if (self.arguments.update_config
|
|
317
|
+
and self.arguments.ignore_bashrc):
|
|
318
|
+
if os.path.isfile(bashrc):
|
|
319
|
+
copy(bashrc, bashrc_back) # Make a backup of bashrc
|
|
320
|
+
self.modify_bashrc_gnuhealth_section(bashrc, "")
|
|
321
|
+
|
|
322
|
+
def modify_bashrc_gnuhealth_section(self, file_path, content):
|
|
323
|
+
|
|
324
|
+
start_marker = "### GNUHEALTH_BEGIN_SETTING ###"
|
|
325
|
+
end_marker = "### GNUHEALTH_END_SETTING ###"
|
|
326
|
+
|
|
327
|
+
start_index = -1
|
|
328
|
+
end_index = -1
|
|
329
|
+
|
|
330
|
+
if os.path.isfile(file_path): # Check for existance of the file
|
|
331
|
+
with open(file_path, 'r') as f:
|
|
332
|
+
lines = f.readlines()
|
|
333
|
+
|
|
334
|
+
for i, line in enumerate(lines):
|
|
335
|
+
if start_marker in line:
|
|
336
|
+
start_index = i
|
|
337
|
+
if end_marker in line:
|
|
338
|
+
end_index = i
|
|
339
|
+
|
|
340
|
+
if start_index == -1 or end_index == -1:
|
|
341
|
+
with open(file_path, "a") as f:
|
|
342
|
+
f.write(f'\n{start_marker}\n{content}\n{end_marker}\n')
|
|
343
|
+
|
|
344
|
+
else:
|
|
345
|
+
new_lines = lines[:start_index + 1] + \
|
|
346
|
+
[content + "\n"] + lines[end_index:]
|
|
347
|
+
|
|
348
|
+
with open(file_path, 'w') as f:
|
|
349
|
+
f.writelines(new_lines)
|
|
350
|
+
|
|
351
|
+
def trytond_cfg(self):
|
|
352
|
+
""" Generate trytond.conf
|
|
353
|
+
and server_log.conf
|
|
354
|
+
"""
|
|
355
|
+
his_dir = self.get_his_dir()
|
|
356
|
+
timestamp = self.get_timestamp()
|
|
357
|
+
trytond_cfg = f"{his_dir}/etc/trytond.conf"
|
|
358
|
+
trytond_cfg_back = f"{his_dir}/etc/trytond.conf.back-{timestamp}"
|
|
359
|
+
|
|
360
|
+
trytond_log_conf = f"{his_dir}/etc/server_log.conf"
|
|
361
|
+
trytond_log_conf_back = \
|
|
362
|
+
f"{his_dir}/etc/server_log.conf.back-{timestamp}"
|
|
363
|
+
|
|
364
|
+
if os.path.isfile(trytond_cfg): # If the profile exists, make a backup
|
|
365
|
+
copy(trytond_cfg, trytond_cfg_back)
|
|
366
|
+
|
|
367
|
+
if os.path.isfile(trytond_log_conf): # If it exists, make a backup
|
|
368
|
+
copy(trytond_log_conf, trytond_log_conf_back)
|
|
369
|
+
|
|
370
|
+
with open(trytond_cfg, "w") as trytondconf_file:
|
|
371
|
+
file_content = f"""\
|
|
372
|
+
# Generated by gnuhealth-control
|
|
373
|
+
[database]
|
|
374
|
+
uri = postgresql://localhost:5432
|
|
375
|
+
path = {his_dir}/attach
|
|
376
|
+
|
|
377
|
+
[web]
|
|
378
|
+
# Listen to all network interfaces.
|
|
379
|
+
listen = 0.0.0.0:8000
|
|
380
|
+
"""
|
|
381
|
+
trytondconf_file.write(file_content)
|
|
382
|
+
|
|
383
|
+
with open(trytond_log_conf, "w") as logconf_file:
|
|
384
|
+
file_content = f"""\
|
|
385
|
+
[formatters]
|
|
386
|
+
keys=simple
|
|
387
|
+
|
|
388
|
+
[handlers]
|
|
389
|
+
keys=rotate,console
|
|
390
|
+
|
|
391
|
+
[loggers]
|
|
392
|
+
keys=root
|
|
393
|
+
|
|
394
|
+
[formatter_simple]
|
|
395
|
+
format=[%(asctime)s] %(levelname)s:%(name)s:%(message)s
|
|
396
|
+
datefmt=%a %b %d %H:%M:%S %Y
|
|
397
|
+
|
|
398
|
+
[handler_rotate]
|
|
399
|
+
class=handlers.TimedRotatingFileHandler
|
|
400
|
+
args=('{his_dir}/log/his_server.log', 'D', 1, 30)
|
|
401
|
+
formatter=simple
|
|
402
|
+
|
|
403
|
+
[handler_console]
|
|
404
|
+
class=StreamHandler
|
|
405
|
+
formatter=simple
|
|
406
|
+
args=(sys.stdout,)
|
|
407
|
+
|
|
408
|
+
[logger_root]
|
|
409
|
+
level=INFO
|
|
410
|
+
handlers=rotate,console
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
logconf_file.write(file_content)
|
|
414
|
+
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
def install_finished(self):
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
def do_task(self, task):
|
|
421
|
+
""" Method to execute each of the tasks
|
|
422
|
+
and get the result code
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
tsk = getattr(self, task)
|
|
426
|
+
rc = tsk()
|
|
427
|
+
return (rc)
|
|
428
|
+
|
|
429
|
+
def setup(self):
|
|
430
|
+
""" Installs a GNU Health HIS Server
|
|
431
|
+
"""
|
|
432
|
+
if self.arguments.update_config:
|
|
433
|
+
TASKS = ["check_config_dir", "gnuhealthrc",
|
|
434
|
+
"trytond_cfg", "install_finished"]
|
|
435
|
+
else:
|
|
436
|
+
TASKS = ["setup_dirs", "setup_virtualenv", "install_core",
|
|
437
|
+
"gnuhealthrc", "trytond_cfg", "install_finished"]
|
|
438
|
+
logwin = log_window()
|
|
439
|
+
row = 1
|
|
440
|
+
|
|
441
|
+
for task in TASKS:
|
|
442
|
+
if task == 'install_finished':
|
|
443
|
+
task_str = "Installation successful"
|
|
444
|
+
msg = "Please LOGOUT and re-login " \
|
|
445
|
+
"to activate the new environment"
|
|
446
|
+
|
|
447
|
+
attr = curses.color_pair(3) | curses.A_BOLD
|
|
448
|
+
x = int(curses.COLS/2 - len(msg)/2)
|
|
449
|
+
msgwin = curses.newwin(4, len(msg)+1, 13, x)
|
|
450
|
+
msgwin.addstr(2, 1, msg, attr)
|
|
451
|
+
msgwin.refresh()
|
|
452
|
+
|
|
453
|
+
else:
|
|
454
|
+
task_str = f"Running {task} ..."
|
|
455
|
+
show_logs(win=logwin, row=row, col=1, data=task_str, attr=0)
|
|
456
|
+
logwin.refresh()
|
|
457
|
+
rc = self.do_task(task)
|
|
458
|
+
if rc == 0:
|
|
459
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
460
|
+
result = "[OK]"
|
|
461
|
+
else:
|
|
462
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
463
|
+
result = "[ERROR]"
|
|
464
|
+
col = 30
|
|
465
|
+
show_logs(
|
|
466
|
+
win=logwin, row=row, col=col, data=result, attr=attr)
|
|
467
|
+
row = row + 1
|
|
468
|
+
logwin.refresh()
|
|
469
|
+
if rc != 0:
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
def start(self):
|
|
473
|
+
""" Start the GNU Health HIS Tryton server
|
|
474
|
+
"""
|
|
475
|
+
nenv = os.environ.copy()
|
|
476
|
+
try:
|
|
477
|
+
subprocess.Popen(
|
|
478
|
+
['trytond'], env=nenv, stdout=subprocess.DEVNULL,
|
|
479
|
+
stderr=subprocess.STDOUT)
|
|
480
|
+
|
|
481
|
+
except BaseException:
|
|
482
|
+
return -1
|
|
483
|
+
|
|
484
|
+
GHControl.footer_window(self, self.stdscr) # Refresh footer window
|
|
485
|
+
return 0
|
|
486
|
+
|
|
487
|
+
def stop(self):
|
|
488
|
+
""" Stops the GNU Health HIS Tryton server
|
|
489
|
+
"""
|
|
490
|
+
for proc in psutil.process_iter():
|
|
491
|
+
# look for combinations that contain both python and trytond in
|
|
492
|
+
# the same command line
|
|
493
|
+
try:
|
|
494
|
+
command_line = proc.cmdline()
|
|
495
|
+
except psutil.ZombieProcess:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
if any("python" and "trytond" in args for args in command_line):
|
|
499
|
+
proc.terminate() # Gracefully ask to stop the server
|
|
500
|
+
try:
|
|
501
|
+
proc.wait(timeout=5)
|
|
502
|
+
except BaseException: # If timeout, time to kill...
|
|
503
|
+
proc.kill()
|
|
504
|
+
proc.wait()
|
|
505
|
+
|
|
506
|
+
GHControl.footer_window(self, self.stdscr) # Refresh footer window
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
def status(self):
|
|
510
|
+
""" Shows the status of the server
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
for proc in psutil.process_iter():
|
|
514
|
+
# True if process contains python & trytond (
|
|
515
|
+
# (eg, python ./trytond // python trytond ..)
|
|
516
|
+
# We can evaluate other combinations, like when using gunicorn
|
|
517
|
+
try:
|
|
518
|
+
command_line = proc.cmdline()
|
|
519
|
+
|
|
520
|
+
except psutil.ZombieProcess:
|
|
521
|
+
pass
|
|
522
|
+
except psutil.NoSuchProcess:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
if any("python" and "trytond" in args for args in command_line):
|
|
526
|
+
return True
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
def update(self):
|
|
530
|
+
""" Updates to the latest compatible version the existing
|
|
531
|
+
gnuhealth and tryton packages in the virtual environment
|
|
532
|
+
"""
|
|
533
|
+
pip_cache_dir = self.get_pip_cache_dir()
|
|
534
|
+
his_dir = self.get_his_dir()
|
|
535
|
+
npython = f"{his_dir}/venv/bin/python"
|
|
536
|
+
updatelog = f"{his_dir}/log/update.log"
|
|
537
|
+
logfile = open(updatelog, "w")
|
|
538
|
+
|
|
539
|
+
win = curses.newwin(10, 50, 3, 30)
|
|
540
|
+
win.box()
|
|
541
|
+
pkg_ok = True
|
|
542
|
+
status = "System Package(s) Update OK"
|
|
543
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
544
|
+
|
|
545
|
+
for package in distributions():
|
|
546
|
+
pkg = package.metadata["Name"]
|
|
547
|
+
win.addstr(1, 2, "Updating....")
|
|
548
|
+
x = 25 - int(len(pkg)/2)
|
|
549
|
+
win.addstr(4, x, f"{pkg}", curses.A_BOLD)
|
|
550
|
+
|
|
551
|
+
win.refresh()
|
|
552
|
+
|
|
553
|
+
if ("tryton" in pkg or "gnuhealth" in pkg):
|
|
554
|
+
if "tryton" in pkg:
|
|
555
|
+
pkg = f"{pkg}{TRYTON_PINNING}"
|
|
556
|
+
else:
|
|
557
|
+
pkg = f"{pkg}{MIN_VER}" # Get GNU Health packages
|
|
558
|
+
|
|
559
|
+
process = [
|
|
560
|
+
npython, '-m', 'pip', 'install', '--cache-dir',
|
|
561
|
+
pip_cache_dir, '--upgrade',
|
|
562
|
+
pkg]
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
subprocess.check_call(
|
|
566
|
+
process, stdout=logfile, stderr=logfile)
|
|
567
|
+
except BaseException:
|
|
568
|
+
pkg_ok = False
|
|
569
|
+
|
|
570
|
+
win.refresh()
|
|
571
|
+
win.clear()
|
|
572
|
+
win.box()
|
|
573
|
+
|
|
574
|
+
if not pkg_ok:
|
|
575
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
576
|
+
status = "[ERROR] Check logs"
|
|
577
|
+
|
|
578
|
+
x = 25 - int(len(status)/2)
|
|
579
|
+
win.addstr(4, x, status, attr)
|
|
580
|
+
win.refresh()
|
|
581
|
+
|
|
582
|
+
def add_package(self):
|
|
583
|
+
""" Get the package name (without the gnuhealth prefix)
|
|
584
|
+
"""
|
|
585
|
+
his_dir = self.get_his_dir()
|
|
586
|
+
pip_cache_dir = self.get_pip_cache_dir()
|
|
587
|
+
npython = f"{his_dir}/venv/bin/python"
|
|
588
|
+
pkglog = f"{his_dir}/log/packages.log"
|
|
589
|
+
|
|
590
|
+
# List the official GNU Health packages
|
|
591
|
+
winwidth = 80
|
|
592
|
+
x = int(curses.COLS/2 - winwidth/2)
|
|
593
|
+
winlist = curses.newwin(12, winwidth, 13, x)
|
|
594
|
+
winlist.box()
|
|
595
|
+
winlist.addstr(1, 2, "Available packages:", curses.color_pair(1))
|
|
596
|
+
row = 3
|
|
597
|
+
col = 2
|
|
598
|
+
|
|
599
|
+
for pkg in HEALTH_PACKAGES:
|
|
600
|
+
if HEALTH_PACKAGES[-1] == pkg:
|
|
601
|
+
pkg = f"{pkg}."
|
|
602
|
+
else:
|
|
603
|
+
pkg = f"{pkg}, "
|
|
604
|
+
|
|
605
|
+
if col + len(pkg) < 80:
|
|
606
|
+
winlist.addstr(row, col, pkg)
|
|
607
|
+
col = col + len(pkg)
|
|
608
|
+
else:
|
|
609
|
+
col = 2
|
|
610
|
+
row = row + 1
|
|
611
|
+
winlist.addstr(row, col, pkg)
|
|
612
|
+
winlist.refresh()
|
|
613
|
+
|
|
614
|
+
win = curses.newwin(10, 50, 3, 30)
|
|
615
|
+
win.box()
|
|
616
|
+
curses.echo()
|
|
617
|
+
curses.curs_set(1)
|
|
618
|
+
win.addstr(2, 1, "Health package:")
|
|
619
|
+
pkg = win.getstr(2, 20).decode()
|
|
620
|
+
win.addstr(4, 1, f"{pkg} selected.")
|
|
621
|
+
win.addstr(6, 1, "Install? (yes/no)")
|
|
622
|
+
confirm = win.getstr(6, 20).decode()
|
|
623
|
+
if (confirm != "yes"):
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
win.clear()
|
|
627
|
+
win.refresh()
|
|
628
|
+
win.box()
|
|
629
|
+
|
|
630
|
+
if (pkg not in HEALTH_PACKAGES and pkg != "all"):
|
|
631
|
+
win.addstr(4, 3, f"ERROR: {pkg} not in list")
|
|
632
|
+
win.refresh()
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
"""
|
|
636
|
+
If we answer yes, use pip to install the selected package
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
curses.curs_set(0)
|
|
640
|
+
|
|
641
|
+
logfile = open(pkglog, "w")
|
|
642
|
+
|
|
643
|
+
if pkg == "all":
|
|
644
|
+
packages = HEALTH_PACKAGES
|
|
645
|
+
else:
|
|
646
|
+
packages = [pkg]
|
|
647
|
+
|
|
648
|
+
pkg_ok = True
|
|
649
|
+
status = "Package(s) Installation OK"
|
|
650
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
651
|
+
for pkg in packages:
|
|
652
|
+
pkg_pinning = f"{pkg}{MIN_VER}" # Append version pinning
|
|
653
|
+
win.addstr(1, 2, "Processing....")
|
|
654
|
+
x = 25 - int(len(pkg)/2)
|
|
655
|
+
win.addstr(4, x, f"{pkg}", curses.A_BOLD)
|
|
656
|
+
|
|
657
|
+
win.refresh()
|
|
658
|
+
|
|
659
|
+
process = [
|
|
660
|
+
npython, '-m', 'pip', 'install', '--cache-dir',
|
|
661
|
+
pip_cache_dir, '--upgrade',
|
|
662
|
+
f"gnuhealth-{pkg_pinning}"]
|
|
663
|
+
|
|
664
|
+
try:
|
|
665
|
+
subprocess.check_call(
|
|
666
|
+
process, stdout=logfile, stderr=logfile)
|
|
667
|
+
except BaseException:
|
|
668
|
+
pkg_ok = False
|
|
669
|
+
|
|
670
|
+
win.refresh()
|
|
671
|
+
win.clear()
|
|
672
|
+
win.box()
|
|
673
|
+
|
|
674
|
+
if not pkg_ok:
|
|
675
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
676
|
+
status = "[ERROR] Check logs"
|
|
677
|
+
|
|
678
|
+
x = 25 - int(len(status)/2)
|
|
679
|
+
win.addstr(4, x, status, attr)
|
|
680
|
+
win.refresh()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class Instance:
|
|
684
|
+
|
|
685
|
+
arguments = None
|
|
686
|
+
|
|
687
|
+
def __init__(self, stdscr, arguments):
|
|
688
|
+
self.arguments = arguments
|
|
689
|
+
self.his_base = os.environ.get('GNUHEALTH_HIS_BASE')
|
|
690
|
+
|
|
691
|
+
def show_logs(self, win, row, col, data, attr=None):
|
|
692
|
+
win.addstr(row, col, data, attr)
|
|
693
|
+
win.refresh()
|
|
694
|
+
|
|
695
|
+
def create_db(self, *args):
|
|
696
|
+
""" Create database
|
|
697
|
+
"""
|
|
698
|
+
dbname, = args[0]
|
|
699
|
+
dblog = f"{self.his_base}/log/createdb.log"
|
|
700
|
+
with open(dblog, "w") as logfile:
|
|
701
|
+
try:
|
|
702
|
+
subprocess.run(
|
|
703
|
+
['createdb', dbname],
|
|
704
|
+
stdout=logfile, stderr=logfile)
|
|
705
|
+
|
|
706
|
+
except BaseException:
|
|
707
|
+
return -1
|
|
708
|
+
return 0
|
|
709
|
+
|
|
710
|
+
def create_instance(self, *args):
|
|
711
|
+
""" Create Instance
|
|
712
|
+
"""
|
|
713
|
+
dbname = args[0][0]
|
|
714
|
+
email = args[0][1]
|
|
715
|
+
password = args[0][2]
|
|
716
|
+
fname = ''.join(random.choices(string.ascii_lowercase, k=5))
|
|
717
|
+
tryton_pass = f"/tmp/.{fname}"
|
|
718
|
+
with open(tryton_pass, "w") as pwfile:
|
|
719
|
+
try:
|
|
720
|
+
pwfile.write(password)
|
|
721
|
+
except BaseException:
|
|
722
|
+
os.remove(tryton_pass) # Delete temp file
|
|
723
|
+
return -1
|
|
724
|
+
|
|
725
|
+
log = f"{self.his_base}/log/create_instance.log"
|
|
726
|
+
tadmin = f"{self.his_base}/venv/bin/trytond-admin"
|
|
727
|
+
|
|
728
|
+
""" Update the environment variables for this session
|
|
729
|
+
"""
|
|
730
|
+
nenv = os.environ.copy()
|
|
731
|
+
nenv['TRYTONPASSFILE'] = tryton_pass
|
|
732
|
+
|
|
733
|
+
with open(log, "w") as logfile:
|
|
734
|
+
try:
|
|
735
|
+
subprocess.check_call(
|
|
736
|
+
[tadmin, '--database', dbname,
|
|
737
|
+
'--email', email, '--all', '-vv'],
|
|
738
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
739
|
+
except BaseException:
|
|
740
|
+
os.remove(tryton_pass)
|
|
741
|
+
return -1
|
|
742
|
+
|
|
743
|
+
os.remove(tryton_pass) # Delete temp file
|
|
744
|
+
return 0
|
|
745
|
+
|
|
746
|
+
def refresh_instance(self, *args):
|
|
747
|
+
""" It updates all the modules
|
|
748
|
+
executting trytond-admin --all
|
|
749
|
+
"""
|
|
750
|
+
win = curses.newwin(10, 50, 3, 30)
|
|
751
|
+
win.box()
|
|
752
|
+
curses.echo()
|
|
753
|
+
curses.curs_set(1)
|
|
754
|
+
win.addstr(2, 1, "Instance Name:")
|
|
755
|
+
dbname = win.getstr(2, 20).decode()
|
|
756
|
+
curses.curs_set(0)
|
|
757
|
+
win.addstr(4, 1, "Refreshing modules...")
|
|
758
|
+
win.refresh()
|
|
759
|
+
|
|
760
|
+
log = f"{self.his_base}/log/refresh_instance.log"
|
|
761
|
+
tadmin = f"{self.his_base}/venv/bin/trytond-admin"
|
|
762
|
+
|
|
763
|
+
""" Update the environment variables for this session
|
|
764
|
+
"""
|
|
765
|
+
nenv = os.environ.copy()
|
|
766
|
+
|
|
767
|
+
with open(log, "w") as logfile:
|
|
768
|
+
try:
|
|
769
|
+
# check_call to get the return code from trytond-admin
|
|
770
|
+
subprocess.check_call(
|
|
771
|
+
[tadmin, '--database', dbname,
|
|
772
|
+
'--all', '-vv'],
|
|
773
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
774
|
+
except BaseException:
|
|
775
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
776
|
+
win.addstr(4, 25, "[ERROR]", attr)
|
|
777
|
+
win.refresh()
|
|
778
|
+
return -1
|
|
779
|
+
|
|
780
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
781
|
+
win.addstr(4, 25, "[OK]", attr)
|
|
782
|
+
win.refresh()
|
|
783
|
+
return 0
|
|
784
|
+
|
|
785
|
+
def install_health_package(self, *args):
|
|
786
|
+
""" Installs the health core package
|
|
787
|
+
"""
|
|
788
|
+
dbname = args[0][0]
|
|
789
|
+
|
|
790
|
+
log = f"{self.his_base}/log/install_health_package.log"
|
|
791
|
+
tadmin = f"{self.his_base}/venv/bin/trytond-admin"
|
|
792
|
+
|
|
793
|
+
""" Retrieve the environment variables for this session
|
|
794
|
+
"""
|
|
795
|
+
nenv = os.environ.copy()
|
|
796
|
+
|
|
797
|
+
with open(log, "w") as logfile:
|
|
798
|
+
try:
|
|
799
|
+
subprocess.check_call(
|
|
800
|
+
[tadmin, '--database', dbname,
|
|
801
|
+
'--update', 'health', '--activate-dependencies', '-vv'],
|
|
802
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
803
|
+
except BaseException:
|
|
804
|
+
return -1
|
|
805
|
+
|
|
806
|
+
return 0
|
|
807
|
+
|
|
808
|
+
def import_countries(self, *args):
|
|
809
|
+
""" Import countries
|
|
810
|
+
"""
|
|
811
|
+
dbname = args[0][0]
|
|
812
|
+
log = f"{self.his_base}/log/import_countries.log"
|
|
813
|
+
imp_countries = f"{self.his_base}/venv/bin/trytond_import_countries"
|
|
814
|
+
|
|
815
|
+
""" Load environment variables for this session
|
|
816
|
+
"""
|
|
817
|
+
nenv = os.environ.copy()
|
|
818
|
+
|
|
819
|
+
with open(log, "w") as logfile:
|
|
820
|
+
try:
|
|
821
|
+
subprocess.check_call(
|
|
822
|
+
[imp_countries, '--database', dbname],
|
|
823
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
824
|
+
except BaseException:
|
|
825
|
+
return -1
|
|
826
|
+
return 0
|
|
827
|
+
|
|
828
|
+
def import_currencies(self, *args):
|
|
829
|
+
""" Import currencies
|
|
830
|
+
"""
|
|
831
|
+
dbname = args[0][0]
|
|
832
|
+
log = f"{self.his_base}/log/import_currencies.log"
|
|
833
|
+
imp_currencies = f"{self.his_base}/venv/bin/trytond_import_currencies"
|
|
834
|
+
|
|
835
|
+
""" Load environment variables for this session
|
|
836
|
+
"""
|
|
837
|
+
nenv = os.environ.copy()
|
|
838
|
+
|
|
839
|
+
with open(log, "w") as logfile:
|
|
840
|
+
try:
|
|
841
|
+
subprocess.check_call(
|
|
842
|
+
[imp_currencies, '--database', dbname],
|
|
843
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
844
|
+
except BaseException:
|
|
845
|
+
return -1
|
|
846
|
+
return 0
|
|
847
|
+
|
|
848
|
+
def backup(self):
|
|
849
|
+
""" Make a backup of the database
|
|
850
|
+
"""
|
|
851
|
+
timestamp = get_timestamp()
|
|
852
|
+
|
|
853
|
+
win = curses.newwin(10, 50, 3, 30)
|
|
854
|
+
win.box()
|
|
855
|
+
curses.echo()
|
|
856
|
+
curses.curs_set(1)
|
|
857
|
+
win.addstr(2, 1, "Instance Name:")
|
|
858
|
+
dbname = win.getstr(2, 20).decode()
|
|
859
|
+
curses.curs_set(0)
|
|
860
|
+
win.addstr(4, 1, "Backing up instance...")
|
|
861
|
+
win.refresh()
|
|
862
|
+
|
|
863
|
+
log = f"{self.his_base}/log/backup_instance.log"
|
|
864
|
+
|
|
865
|
+
""" Update the environment variables for this session
|
|
866
|
+
"""
|
|
867
|
+
nenv = os.environ.copy()
|
|
868
|
+
|
|
869
|
+
bdest = f"{self.his_base}/backup/{dbname}-{timestamp}.sql"
|
|
870
|
+
with open(log, "w") as logfile:
|
|
871
|
+
try:
|
|
872
|
+
# check_call to get the return code from trytond-admin
|
|
873
|
+
subprocess.check_call(
|
|
874
|
+
['pg_dump', '--dbname', dbname,
|
|
875
|
+
'--file', bdest],
|
|
876
|
+
env=nenv, stdout=logfile, stderr=logfile)
|
|
877
|
+
except BaseException:
|
|
878
|
+
attr = curses.color_pair(2) | curses.A_BOLD
|
|
879
|
+
win.addstr(4, 25, "[ERROR]", attr)
|
|
880
|
+
win.refresh()
|
|
881
|
+
return -1
|
|
882
|
+
|
|
883
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
884
|
+
win.addstr(4, 25, "[OK]", attr)
|
|
885
|
+
win.refresh()
|
|
886
|
+
return 0
|
|
887
|
+
|
|
888
|
+
def command_successful(self, *args):
|
|
889
|
+
return 0
|
|
890
|
+
|
|
891
|
+
def new(self, dbname=None):
|
|
892
|
+
""" Get the instance/db name, admin password and email
|
|
893
|
+
"""
|
|
894
|
+
win = curses.newwin(10, 50, 3, 30)
|
|
895
|
+
win.box()
|
|
896
|
+
curses.echo()
|
|
897
|
+
curses.curs_set(1)
|
|
898
|
+
win.addstr(2, 1, "Instance Name:")
|
|
899
|
+
db = win.getstr(2, 20).decode()
|
|
900
|
+
win.addstr(3, 1, "Password:")
|
|
901
|
+
password = win.getstr(3, 20).decode()
|
|
902
|
+
win.addstr(4, 1, "email:")
|
|
903
|
+
email = win.getstr(4, 20).decode()
|
|
904
|
+
win.addstr(6, 1, "Ready? (yes/no)")
|
|
905
|
+
confirm = win.getstr(6, 20).decode()
|
|
906
|
+
if (confirm != "yes"):
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
"""
|
|
910
|
+
If we answer yes, go ahead with the instance creation
|
|
911
|
+
"""
|
|
912
|
+
curses.curs_set(0)
|
|
913
|
+
win.clear()
|
|
914
|
+
win.refresh()
|
|
915
|
+
logwin = log_window()
|
|
916
|
+
logwin.refresh()
|
|
917
|
+
|
|
918
|
+
""" TASKS is an list of tuples, each element containing
|
|
919
|
+
the method and it's arguments
|
|
920
|
+
"""
|
|
921
|
+
TASKS = [('create_db', [db]),
|
|
922
|
+
('create_instance', [db, email, password]),
|
|
923
|
+
('install_health_package', [db]),
|
|
924
|
+
('import_countries', [db]),
|
|
925
|
+
('import_currencies', [db]),
|
|
926
|
+
('command_successful', []),
|
|
927
|
+
]
|
|
928
|
+
row = 0
|
|
929
|
+
for task in TASKS:
|
|
930
|
+
row = row + 1
|
|
931
|
+
tsk = getattr(self, task[0])
|
|
932
|
+
# Pass the string and the actual method as arguments
|
|
933
|
+
rc = task_engine(task=(task, tsk), window=logwin, row=row)
|
|
934
|
+
if rc != 0:
|
|
935
|
+
break
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class Help:
|
|
939
|
+
|
|
940
|
+
def display():
|
|
941
|
+
""" show the help window
|
|
942
|
+
"""
|
|
943
|
+
pass
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class MenuPanel:
|
|
947
|
+
def __init__(self, entries, win, footer, title, leaf=False, action=None):
|
|
948
|
+
curses.start_color()
|
|
949
|
+
# Init color pairs
|
|
950
|
+
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # OK
|
|
951
|
+
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED) # ERROR
|
|
952
|
+
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # WARNING
|
|
953
|
+
self.window = win
|
|
954
|
+
self.window_title = title
|
|
955
|
+
self.panel = panel.new_panel(self.window)
|
|
956
|
+
self.panel.hide()
|
|
957
|
+
self.footer = footer
|
|
958
|
+
self.footer_panel = panel.new_panel(self.footer)
|
|
959
|
+
self.leaf = leaf
|
|
960
|
+
self.action = action
|
|
961
|
+
panel.update_panels()
|
|
962
|
+
|
|
963
|
+
self.position = 0
|
|
964
|
+
self.entries = entries
|
|
965
|
+
|
|
966
|
+
def centerx(self, msg):
|
|
967
|
+
x = int(curses.COLS / 2) - int(len(msg) / 2)
|
|
968
|
+
return x
|
|
969
|
+
|
|
970
|
+
def menu(self):
|
|
971
|
+
""" Show the menu of the specific section
|
|
972
|
+
"""
|
|
973
|
+
self.window.clear()
|
|
974
|
+
self.window.addstr(
|
|
975
|
+
0, self.centerx(self.window_title), self.window_title)
|
|
976
|
+
self.panel.top()
|
|
977
|
+
self.panel.show()
|
|
978
|
+
|
|
979
|
+
""" Main menu loop"""
|
|
980
|
+
while True:
|
|
981
|
+
self.window.refresh()
|
|
982
|
+
curses.doupdate()
|
|
983
|
+
for index, entry in enumerate(self.entries):
|
|
984
|
+
if index == self.position:
|
|
985
|
+
mode = curses.A_REVERSE
|
|
986
|
+
else:
|
|
987
|
+
mode = curses.A_NORMAL
|
|
988
|
+
|
|
989
|
+
entry_desc = f"{index}: {entry[0]}"
|
|
990
|
+
self.window.addstr(5 + index, 2, entry_desc, mode)
|
|
991
|
+
|
|
992
|
+
self.window.hline(1, 0, curses.ACS_HLINE, curses.COLS)
|
|
993
|
+
|
|
994
|
+
key = self.window.getch()
|
|
995
|
+
|
|
996
|
+
if key == ord("\n"): # Map the Enter key
|
|
997
|
+
# Exec the method associated to the entry
|
|
998
|
+
if not self.leaf:
|
|
999
|
+
if self.entries[self.position][1]:
|
|
1000
|
+
self.entries[self.position][1]()
|
|
1001
|
+
else:
|
|
1002
|
+
self.entries[self.position][1]()
|
|
1003
|
+
|
|
1004
|
+
if key == ord("q"):
|
|
1005
|
+
break
|
|
1006
|
+
|
|
1007
|
+
if key == ord("h"):
|
|
1008
|
+
Help.display()
|
|
1009
|
+
|
|
1010
|
+
if key == curses.KEY_UP and self.position > 0:
|
|
1011
|
+
self.position = self.position - 1
|
|
1012
|
+
|
|
1013
|
+
if (key == curses.KEY_DOWN
|
|
1014
|
+
and self.position < len(self.entries) - 1):
|
|
1015
|
+
self.position = self.position + 1
|
|
1016
|
+
|
|
1017
|
+
if chr(key).isnumeric():
|
|
1018
|
+
entry_nr = int(chr(key))
|
|
1019
|
+
if entry_nr in range(0, len(self.entries)):
|
|
1020
|
+
self.position = int(chr(key))
|
|
1021
|
+
else:
|
|
1022
|
+
# Out of range
|
|
1023
|
+
self.menu_error()
|
|
1024
|
+
|
|
1025
|
+
self.window.clear()
|
|
1026
|
+
self.panel.hide()
|
|
1027
|
+
panel.update_panels()
|
|
1028
|
+
curses.doupdate()
|
|
1029
|
+
|
|
1030
|
+
def menu_error(self):
|
|
1031
|
+
curses.beep()
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
class GHControl:
|
|
1035
|
+
def __init__(self, stdscr, arguments):
|
|
1036
|
+
curses.curs_set(False) # Disable cursor
|
|
1037
|
+
basedir = arguments.basedir
|
|
1038
|
+
|
|
1039
|
+
self.win = self.main_window(stdscr)
|
|
1040
|
+
self.footer = self.footer_window(stdscr)
|
|
1041
|
+
server = Server(self, arguments=arguments)
|
|
1042
|
+
instance = Instance(self, arguments=arguments)
|
|
1043
|
+
installation_entries = [
|
|
1044
|
+
("Start Installation", server.setup, arguments),]
|
|
1045
|
+
installation_section = MenuPanel(
|
|
1046
|
+
installation_entries, self.win, self.footer,
|
|
1047
|
+
f"GNU Health HIS Installation (basedir = {basedir})", leaf=True)
|
|
1048
|
+
|
|
1049
|
+
update_entries = [
|
|
1050
|
+
("Start Packages Update", server.update, arguments),]
|
|
1051
|
+
update_section = MenuPanel(
|
|
1052
|
+
update_entries, self.win, self.footer,
|
|
1053
|
+
"GNU Health HIS Packages Update", leaf=True)
|
|
1054
|
+
|
|
1055
|
+
package_entries = [
|
|
1056
|
+
("Add Health package", server.add_package, arguments),]
|
|
1057
|
+
package_section = MenuPanel(
|
|
1058
|
+
package_entries, self.win, self.footer,
|
|
1059
|
+
"GNU Health HIS Add Package", leaf=True)
|
|
1060
|
+
|
|
1061
|
+
instance_entries = [
|
|
1062
|
+
("New Instance", instance.new, arguments),]
|
|
1063
|
+
instance_section = MenuPanel(
|
|
1064
|
+
instance_entries, self.win, self.footer,
|
|
1065
|
+
"GNU Health Instance", leaf=True)
|
|
1066
|
+
|
|
1067
|
+
refresh_entries = [
|
|
1068
|
+
("Refresh Instance", instance.refresh_instance),]
|
|
1069
|
+
refresh_section = MenuPanel(
|
|
1070
|
+
refresh_entries, self.win, self.footer,
|
|
1071
|
+
"GNU Health Instance", leaf=True)
|
|
1072
|
+
|
|
1073
|
+
backup_entries = [("DB Instance Backup", instance.backup),]
|
|
1074
|
+
backup_section = MenuPanel(
|
|
1075
|
+
backup_entries, self.win, self.footer, "Backup", leaf=True)
|
|
1076
|
+
|
|
1077
|
+
startstop_entries = [
|
|
1078
|
+
("Start GNU Health HIS", server.start),
|
|
1079
|
+
("Stop Server", server.stop),
|
|
1080
|
+
]
|
|
1081
|
+
startstop_section = MenuPanel(
|
|
1082
|
+
startstop_entries, self.win, self.footer,
|
|
1083
|
+
"Server start / stop", leaf=True)
|
|
1084
|
+
|
|
1085
|
+
main_entries = [
|
|
1086
|
+
("Install GNU Health HIS", installation_section.menu),
|
|
1087
|
+
("Create a new DB instance", instance_section.menu),
|
|
1088
|
+
("Add Health package", package_section.menu),
|
|
1089
|
+
("Refresh DB instance", refresh_section.menu),
|
|
1090
|
+
("Start / stop instance", startstop_section.menu),
|
|
1091
|
+
("Update packages / dependencies", update_section.menu),
|
|
1092
|
+
("Backup instance", backup_section.menu),
|
|
1093
|
+
]
|
|
1094
|
+
|
|
1095
|
+
main_section = MenuPanel(
|
|
1096
|
+
main_entries, self.win, self.footer,
|
|
1097
|
+
f"Welcome to GNU Health Control Center {VERSION}")
|
|
1098
|
+
|
|
1099
|
+
main_section.menu()
|
|
1100
|
+
|
|
1101
|
+
def main_window(self, stdscr):
|
|
1102
|
+
""" We'll define a main window
|
|
1103
|
+
"""
|
|
1104
|
+
win = curses.newwin(curses.LINES - 5, curses.COLS, 0, 0)
|
|
1105
|
+
win.keypad(True) # Need keypad to map KEY_[UP|DOWN]
|
|
1106
|
+
return win
|
|
1107
|
+
|
|
1108
|
+
def footer_window(self, stdscr):
|
|
1109
|
+
""" A footer for helpers
|
|
1110
|
+
"""
|
|
1111
|
+
footer = curses.newwin(5, curses.COLS, curses.LINES - 5, 0)
|
|
1112
|
+
footer.box()
|
|
1113
|
+
server_status = Server.status(self)
|
|
1114
|
+
|
|
1115
|
+
if server_status:
|
|
1116
|
+
attr = curses.color_pair(1) | curses.A_BOLD
|
|
1117
|
+
status = "running"
|
|
1118
|
+
else:
|
|
1119
|
+
attr = curses.color_pair(3) | curses.A_BOLD
|
|
1120
|
+
status = "stopped"
|
|
1121
|
+
|
|
1122
|
+
footer_status = "Server status:"
|
|
1123
|
+
footer_hlp = "Press 'q' to go back or exit"
|
|
1124
|
+
footer.addstr(
|
|
1125
|
+
1, int(curses.COLS / 2 - len(footer_status + status) / 2),
|
|
1126
|
+
footer_status)
|
|
1127
|
+
|
|
1128
|
+
footer.addstr(
|
|
1129
|
+
1,
|
|
1130
|
+
int(
|
|
1131
|
+
curses.COLS / 2 - len(footer_status + status) / 2)
|
|
1132
|
+
+ len(footer_status) + 1, status, attr)
|
|
1133
|
+
|
|
1134
|
+
footer.addstr(3, int(curses.COLS / 2 - len(footer_hlp) / 2),
|
|
1135
|
+
footer_hlp)
|
|
1136
|
+
|
|
1137
|
+
footer.refresh()
|
|
1138
|
+
return footer
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def cmdline_args():
|
|
1142
|
+
parser = argparse.ArgumentParser()
|
|
1143
|
+
|
|
1144
|
+
parser.add_argument('-b', '--basedir', default='/opt/gnuhealth',
|
|
1145
|
+
help="Base directory of installation for "
|
|
1146
|
+
"the gnuhealth components\n"
|
|
1147
|
+
"default=/opt/gnuhealth.")
|
|
1148
|
+
|
|
1149
|
+
parser.add_argument('-r', '--release',
|
|
1150
|
+
help="GNU Health release (major and minor numbers), "
|
|
1151
|
+
"It will affect the installation directory of his, "
|
|
1152
|
+
"for example: his-50.")
|
|
1153
|
+
|
|
1154
|
+
parser.add_argument('-i', '--ignore-bashrc', action="store_true",
|
|
1155
|
+
help="Does not modify .bashrc, "
|
|
1156
|
+
"If users want to install different HIS versions. "
|
|
1157
|
+
"If used, the user should use '/path/to/ghis_env'\n "
|
|
1158
|
+
"For example: "
|
|
1159
|
+
"'/opt/gnuhealth/his-50/ghis_env trytond', "
|
|
1160
|
+
"'/opt/gnuhealth/his-50/ghis_env trytond-admin', "
|
|
1161
|
+
"'/opt/gnuhealth/his-50/ghis_env pip install pandas'")
|
|
1162
|
+
|
|
1163
|
+
parser.add_argument('-u', '--update-config', action="store_true",
|
|
1164
|
+
help="Updates configuration files, namely "
|
|
1165
|
+
"tryton.cfg, gnuhealthrc and bashrc, during "
|
|
1166
|
+
"the server installation process.")
|
|
1167
|
+
|
|
1168
|
+
parser.add_argument('-t', '--test', action="store_true",
|
|
1169
|
+
help="Test mode. Uses the repository "
|
|
1170
|
+
"test.pypi.org ")
|
|
1171
|
+
|
|
1172
|
+
parser.add_argument('-p', '--pre', action="store_true",
|
|
1173
|
+
help="Use pre-release packages. Usually used in "
|
|
1174
|
+
"combination with --test")
|
|
1175
|
+
|
|
1176
|
+
return parser.parse_args()
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def main():
|
|
1180
|
+
arguments = cmdline_args()
|
|
1181
|
+
wrapper(GHControl, arguments)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
if __name__ == "__main__":
|
|
1185
|
+
main()
|