gnuhealth-control 5.0.0a9__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.0a9
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
+ You can install the GNU Health HMIS client using your distro package or via pip::
48
+
49
+ $ pip install --upgrade gnuhealth-control
50
+
51
+
52
+ Technology
53
+ ----------
54
+ The GNU Health HMIS client 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
+ You can install the GNU Health HMIS client using your distro package or via pip::
15
+
16
+ $ pip install --upgrade gnuhealth-control
17
+
18
+
19
+ Technology
20
+ ----------
21
+ The GNU Health HMIS client 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.0a9"
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'
@@ -0,0 +1,864 @@
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
+
18
+ import argparse
19
+ import curses
20
+ import os
21
+ import re
22
+ # from time import sleep
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.0a9" # gnuheath-control version
30
+ MIN_VER = 'gnuhealth>=5.0.0a1' # Initial gnuhealth server package version
31
+
32
+
33
+ def log_window():
34
+ logwin = curses.newwin(10, 45, 2, 30)
35
+ logwin.box()
36
+ curses.echo()
37
+ return logwin
38
+
39
+
40
+ def show_logs(win, row, col, data, attr=None):
41
+ win.addstr(row, col, data, attr)
42
+
43
+
44
+ def task_engine(task, window, row):
45
+ task_name = task[0][0]
46
+ args = task[0][1]
47
+ tsk = task[1]
48
+
49
+ task_str = f"Running {task_name} ..."
50
+ show_logs(win=window, row=row, col=1, data=task_str, attr=0)
51
+ window.refresh()
52
+
53
+ rc = tsk(args)
54
+ if rc == 0:
55
+ attr = curses.color_pair(1) | curses.A_BOLD
56
+ result = "[OK]"
57
+ else:
58
+ attr = curses.color_pair(2) | curses.A_BOLD
59
+ result = "[ERROR]"
60
+ col = 35
61
+ show_logs(win=window, row=row, col=col, data=result, attr=attr)
62
+ window.refresh()
63
+ return rc
64
+
65
+
66
+ class Server:
67
+
68
+ arguments = None
69
+
70
+ def __init__(self, stdscr, arguments):
71
+ self.arguments = arguments
72
+
73
+ def show_logs(self, win, row, col, data, attr=None):
74
+ win.addstr(row, col, data, attr)
75
+
76
+ def get_base_dir(self):
77
+ basedir = self.arguments.basedir
78
+ return basedir
79
+
80
+ def get_his_dir(self):
81
+ basedir = self.get_base_dir()
82
+ major_minor = re.match(r'(^[0-9]+)\.([0-9]+)\.(.+$)', VERSION)
83
+ rel = f"{major_minor[1]}{major_minor[2]}"
84
+ release = self.arguments.release or rel
85
+ his_dir = f"{basedir}/his-{release}"
86
+ return his_dir
87
+
88
+ def get_pip_cache_dir(self):
89
+ basedir = self.get_base_dir()
90
+ pip_cache_dir = f"{basedir}/cache/pip"
91
+ return pip_cache_dir
92
+
93
+ def setup_dirs(self):
94
+ """ Creates the basic directory structure to hold GH HIS
95
+ components
96
+ """
97
+
98
+ basedir = self.get_base_dir()
99
+ his_dir = self.get_his_dir()
100
+ pip_cache_dir = self.get_pip_cache_dir()
101
+
102
+ try:
103
+ # Create root directory
104
+ os.makedirs(basedir)
105
+
106
+ # Create pip cache directory
107
+ os.makedirs(pip_cache_dir, exist_ok=True)
108
+
109
+ except FileExistsError: # Allow the basedir to exist
110
+ pass
111
+
112
+ try:
113
+ # Create his directory
114
+ os.mkdir(his_dir)
115
+
116
+ except BaseException:
117
+ return -1
118
+
119
+ for subdir in ["etc", "log", "local", "attach"]:
120
+ try:
121
+ # Create subdirs
122
+ os.mkdir(f"{his_dir}/{subdir}")
123
+ except BaseException:
124
+ return -1
125
+
126
+ return 0
127
+
128
+ def setup_virtualenv(self):
129
+ """ Create the virtual environment
130
+ """
131
+ his_dir = self.get_his_dir()
132
+ venvdir = f"{his_dir}/venv"
133
+ ghvenv = EnvBuilder(system_site_packages=False, with_pip=True)
134
+
135
+ try:
136
+ ghvenv.create(env_dir=venvdir)
137
+ except BaseException:
138
+ return -1
139
+
140
+ return 0
141
+
142
+ def install_core(self):
143
+ """ Install the dependencies in the newly created virtual environment
144
+ """
145
+ his_dir = self.get_his_dir()
146
+ pip_cache_dir = self.get_pip_cache_dir()
147
+ npython = f"{his_dir}/venv/bin/python"
148
+
149
+ """ The following vars will only be used in development
150
+ """
151
+ pre = self.arguments.pre
152
+ test_repo = "https://test.pypi.org/simple/"
153
+ extra_index = "https://pypi.org/simple/"
154
+ deplog = f"{his_dir}/log/install_deps.log"
155
+ with open(deplog, "w") as logfile:
156
+ if (self.arguments.test):
157
+ process = [
158
+ npython, '-m', 'pip', 'install', '-i', test_repo,
159
+ '--extra-index-url', extra_index,
160
+ '--cache-dir', pip_cache_dir,
161
+ '--upgrade', MIN_VER]
162
+ else:
163
+ process = [
164
+ npython, '-m', 'pip', 'install', '--cache-dir',
165
+ pip_cache_dir, '--upgrade',
166
+ MIN_VER]
167
+
168
+ if pre:
169
+ process.append('--pre') # Append pip pre-release argument
170
+
171
+ try:
172
+ subprocess.run(process, stdout=logfile, stderr=logfile)
173
+
174
+ except BaseException:
175
+ return -1
176
+
177
+ return 0
178
+
179
+ def check_config_dir(self):
180
+ his_dir = self.get_his_dir()
181
+ etc_dir = f"{his_dir}/etc"
182
+
183
+ if os.path.exists(etc_dir) and os.path.isdir(etc_dir):
184
+ return 0
185
+ else:
186
+ return -1
187
+
188
+ def gnuhealthrc(self):
189
+ """ Generate the gnuhealthrc profile.
190
+ We explicitly set the virtual environment (VIRTUAL_ENV)
191
+ and path so no need to source 'activate' script
192
+ Set aliases and editor for interactive sessions
193
+ For non-interactive sessions there are also the following
194
+ scripts:
195
+ * start_gnuhealth
196
+ * editconf
197
+ * ghis_env
198
+ """
199
+ his_dir = self.get_his_dir()
200
+ venvdir = f"{his_dir}/venv"
201
+ trytond_cfg = f"{his_dir}/etc/trytond.conf"
202
+ trytond_log_conf = f"{his_dir}/etc/server_log.conf"
203
+
204
+ ghrc = f'{his_dir}/etc/gnuhealthrc'
205
+ self.create_bash_wrap(
206
+ file_name="etc/gnuhealthrc",
207
+ executable=False,
208
+ need_backup=True,
209
+ file_content=f"""\
210
+ export VIRTUAL_ENV={venvdir}
211
+ export PATH={his_dir}:{venvdir}/bin:$PATH
212
+ export TRYTOND_CONFIG={trytond_cfg}
213
+ export TRYTOND_LOGGING_CONFIG={trytond_log_conf}
214
+ export GNUHEALTH_HIS_BASE={his_dir}
215
+
216
+ alias cdlogs='cd {his_dir}/log'
217
+ alias cdbase='cd {his_dir}'
218
+
219
+ # Avoid accidental execution of rm, mv or cp
220
+ alias | grep rm= &> /dev/null || alias rm='rm -i'
221
+ alias | grep mv= &> /dev/null || alias mv='mv -i'
222
+ alias | grep cp= &> /dev/null || alias cp='cp -i'
223
+ """)
224
+
225
+ ghenv = f'{his_dir}/ghis_env'
226
+ self.create_bash_wrap("ghis_env", f"""\
227
+ #!/usr/bin/env bash
228
+
229
+ [[ -f {venvdir}/bin/activate ]] && source {venvdir}/bin/activate
230
+ [[ -f {ghrc} ]] && source {ghrc}
231
+
232
+ export GNUHEALTH_ENV=1
233
+
234
+ exec \"$@\"
235
+ """)
236
+ self.create_bash_wrap("start_gnuhealth", f"""\
237
+ #!/usr/bin/env bash
238
+
239
+ {ghenv} trytond \"$@\"
240
+ """)
241
+
242
+ self.create_bash_wrap("editconf", f"""\
243
+ #!/usr/bin/env bash
244
+
245
+ if command -v nano &> /dev/null; then
246
+ export EDITOR=nano
247
+ else
248
+ export EDITOR=vi
249
+ fi
250
+
251
+ $EDITOR {trytond_cfg} \"$@\"
252
+ """)
253
+
254
+ self.modify_bashrc(ghrc)
255
+
256
+ return 0
257
+
258
+ def create_bash_wrap(self, file_name, file_content,
259
+ executable=True,
260
+ need_backup=False):
261
+ his_dir = self.get_his_dir()
262
+ file_path = f'{his_dir}/{file_name}'
263
+ timestamp = self.get_timestamp()
264
+
265
+ if need_backup:
266
+ file_backup = f"{file_path}.back-{timestamp}"
267
+ if os.path.isfile(file_path):
268
+ copy(file_path, file_backup)
269
+
270
+ with open(file_path, "w") as f:
271
+ file_content = file_content
272
+ f.write(file_content)
273
+ if executable:
274
+ os.chmod(file_path, 0o755)
275
+
276
+ def get_timestamp(self):
277
+ return datetime.now().strftime("%Y%m%d%H%M%S")
278
+
279
+ def modify_bashrc(self, ghrc):
280
+ home = os.environ['HOME']
281
+ bashrc = f"{home}/.bashrc"
282
+ timestamp = self.get_timestamp()
283
+ bashrc_back = f"{home}/.bashrc.back-{timestamp}"
284
+
285
+ if not self.arguments.ignore_bashrc:
286
+ if os.path.isfile(bashrc):
287
+ copy(bashrc, bashrc_back) # Make a backup of bashrc
288
+ self.modify_bashrc_gnuhealth_section(
289
+ bashrc, f"[[ -f {ghrc} ]] && source {ghrc}")
290
+
291
+ # WARNING: if use --update-configs, we will always change
292
+ # $HOME/.bashrc, make sure old bashrc config can be cleaned
293
+ # up.
294
+ if (self.arguments.update_configs
295
+ and self.arguments.ignore_bashrc):
296
+ if os.path.isfile(bashrc):
297
+ copy(bashrc, bashrc_back) # Make a backup of bashrc
298
+ self.modify_bashrc_gnuhealth_section(bashrc, "")
299
+
300
+ def modify_bashrc_gnuhealth_section(self, file_path, content):
301
+
302
+ start_marker = "### GNUHEALTH_BEGIN_SETTING ###"
303
+ end_marker = "### GNUHEALTH_END_SETTING ###"
304
+
305
+ start_index = -1
306
+ end_index = -1
307
+
308
+ if os.path.isfile(file_path): # Check for existance of the file
309
+ with open(file_path, 'r') as f:
310
+ lines = f.readlines()
311
+
312
+ for i, line in enumerate(lines):
313
+ if start_marker in line:
314
+ start_index = i
315
+ if end_marker in line:
316
+ end_index = i
317
+
318
+ if start_index == -1 or end_index == -1:
319
+ with open(file_path, "a") as f:
320
+ f.write(f'\n{start_marker}\n{content}\n{end_marker}\n')
321
+
322
+ else:
323
+ new_lines = lines[:start_index + 1] + \
324
+ [content + "\n"] + lines[end_index:]
325
+
326
+ with open(file_path, 'w') as f:
327
+ f.writelines(new_lines)
328
+
329
+ def trytond_cfg(self):
330
+ """ Generate trytond.conf
331
+ and server_log.conf
332
+ """
333
+ his_dir = self.get_his_dir()
334
+ timestamp = self.get_timestamp()
335
+ trytond_cfg = f"{his_dir}/etc/trytond.conf"
336
+ trytond_cfg_back = f"{his_dir}/etc/trytond.conf.back-{timestamp}"
337
+
338
+ trytond_log_conf = f"{his_dir}/etc/server_log.conf"
339
+ trytond_log_conf_back = \
340
+ f"{his_dir}/etc/server_log.conf.back-{timestamp}"
341
+
342
+ if os.path.isfile(trytond_cfg): # If the profile exists, make a backup
343
+ copy(trytond_cfg, trytond_cfg_back)
344
+
345
+ if os.path.isfile(trytond_log_conf): # If it exists, make a backup
346
+ copy(trytond_log_conf, trytond_log_conf_back)
347
+
348
+ with open(trytond_cfg, "w") as trytondconf_file:
349
+ file_content = f"""\
350
+ # Generated by gnuhealth-control
351
+ [database]
352
+ uri = postgresql://localhost:5432
353
+ path = {his_dir}/attach
354
+
355
+ [web]
356
+ # Listen to all network interfaces.
357
+ listen = 0.0.0.0:8000
358
+ """
359
+ trytondconf_file.write(file_content)
360
+
361
+ with open(trytond_log_conf, "w") as logconf_file:
362
+ file_content = f"""\
363
+ [formatters]
364
+ keys=simple
365
+
366
+ [handlers]
367
+ keys=rotate,console
368
+
369
+ [loggers]
370
+ keys=root
371
+
372
+ [formatter_simple]
373
+ format=[%(asctime)s] %(levelname)s:%(name)s:%(message)s
374
+ datefmt=%a %b %d %H:%M:%S %Y
375
+
376
+ [handler_rotate]
377
+ class=handlers.TimedRotatingFileHandler
378
+ args=('{his_dir}/log/his_server.log', 'D', 1, 30)
379
+ formatter=simple
380
+
381
+ [handler_console]
382
+ class=StreamHandler
383
+ formatter=simple
384
+ args=(sys.stdout,)
385
+
386
+ [logger_root]
387
+ level=INFO
388
+ handlers=rotate,console
389
+ """
390
+
391
+ logconf_file.write(file_content)
392
+
393
+ return 0
394
+
395
+ def install_finished(self):
396
+ return 0
397
+
398
+ def do_task(self, task):
399
+ """ Method to execute each of the tasks
400
+ and get the result code
401
+ """
402
+
403
+ tsk = getattr(self, task)
404
+ rc = tsk()
405
+ return (rc)
406
+
407
+ def setup(self):
408
+ """ Installs a GNU Health HIS Server
409
+ """
410
+ if self.arguments.update_configs:
411
+ TASKS = ["check_config_dir", "gnuhealthrc",
412
+ "trytond_cfg", "install_finished"]
413
+ else:
414
+ TASKS = ["setup_dirs", "setup_virtualenv", "install_core",
415
+ "gnuhealthrc", "trytond_cfg", "install_finished"]
416
+ logwin = log_window()
417
+ row = 1
418
+
419
+ for task in TASKS:
420
+ if task == 'install_finished':
421
+ task_str = "Installation successful"
422
+ else:
423
+ task_str = f"Running {task} ..."
424
+ self.show_logs(win=logwin, row=row, col=1, data=task_str, attr=0)
425
+ logwin.refresh()
426
+ rc = self.do_task(task)
427
+ if rc == 0:
428
+ attr = curses.color_pair(1) | curses.A_BOLD
429
+ result = "[OK]"
430
+ else:
431
+ attr = curses.color_pair(2) | curses.A_BOLD
432
+ result = "[ERROR]"
433
+ col = 30
434
+ self.show_logs(
435
+ win=logwin, row=row, col=col, data=result, attr=attr)
436
+ row = row + 1
437
+ logwin.refresh()
438
+ if rc != 0:
439
+ break
440
+
441
+ def start(self):
442
+ """ Start the GNU Health HIS Tryton server
443
+ """
444
+ pass
445
+
446
+ def status(self):
447
+ """ Shows the status of the server
448
+ """
449
+ for proc in psutil.process_iter():
450
+ # True if process contains python & trytond (
451
+ # (eg, python ./trytond // python trytond ..)
452
+ # We can evaluate other combinations, like when using gunicorn
453
+ if any("python" and "trytond" in args for args in proc.cmdline()):
454
+ return True
455
+ return False
456
+
457
+
458
+ class Instance:
459
+
460
+ arguments = None
461
+
462
+ def __init__(self, stdscr, arguments):
463
+ self.arguments = arguments
464
+ self.his_base = os.environ.get('GNUHEALTH_HIS_BASE')
465
+
466
+ def show_logs(self, win, row, col, data, attr=None):
467
+ win.addstr(row, col, data, attr)
468
+
469
+ def create_db(self, *args):
470
+ """ Create database
471
+ """
472
+ dbname, = args[0]
473
+ dblog = f"{self.his_base}/log/createdb.log"
474
+ with open(dblog, "w") as logfile:
475
+ try:
476
+ subprocess.run(
477
+ ['createdb', dbname],
478
+ stdout=logfile, stderr=logfile)
479
+
480
+ except BaseException:
481
+ return -1
482
+ return 0
483
+
484
+ def create_instance(self, *args):
485
+ """ Create Instance
486
+ """
487
+ dbname = args[0][0]
488
+ email = args[0][1]
489
+ password = args[0][2]
490
+ fname = ''.join(random.choices(string.ascii_lowercase, k=5))
491
+ tryton_pass = f"/tmp/.{fname}"
492
+ with open(tryton_pass, "w") as pwfile:
493
+ try:
494
+ pwfile.write(password)
495
+ except BaseException:
496
+ os.remove(tryton_pass) # Delete temp file
497
+ return -1
498
+
499
+ log = f"{self.his_base}/log/create_instance.log"
500
+ tadmin = f"{self.his_base}/venv/bin/trytond-admin"
501
+
502
+ """ Update the environment variables for this session
503
+ """
504
+ nenv = os.environ.copy()
505
+ nenv['TRYTONPASSFILE'] = tryton_pass
506
+
507
+ with open(log, "w") as logfile:
508
+ try:
509
+ subprocess.run(
510
+ [tadmin, '--database', dbname,
511
+ '--email', email, '--all', '-vv'],
512
+ env=nenv, stdout=logfile, stderr=logfile)
513
+ except BaseException:
514
+ os.remove(tryton_pass)
515
+ return -1
516
+
517
+ os.remove(tryton_pass) # Delete temp file
518
+ return 0
519
+
520
+ def install_health_package(self, *args):
521
+ """ Installs the health core package
522
+ """
523
+ dbname = args[0][0]
524
+
525
+ log = f"{self.his_base}/log/install_health_package.log"
526
+ tadmin = f"{self.his_base}/venv/bin/trytond-admin"
527
+
528
+ """ Retrieve the environment variables for this session
529
+ """
530
+ nenv = os.environ.copy()
531
+
532
+ with open(log, "w") as logfile:
533
+ try:
534
+ subprocess.run(
535
+ [tadmin, '--database', dbname,
536
+ '--update', 'health', '--activate-dependencies', '-vv'],
537
+ env=nenv, stdout=logfile, stderr=logfile)
538
+ except BaseException:
539
+ return -1
540
+
541
+ return 0
542
+
543
+ def import_countries(self, *args):
544
+ """ Import countries
545
+ """
546
+ dbname = args[0][0]
547
+ log = f"{self.his_base}/log/import_countries.log"
548
+ imp_countries = f"{self.his_base}/venv/bin/trytond_import_countries"
549
+
550
+ """ Load environment variables for this session
551
+ """
552
+ nenv = os.environ.copy()
553
+
554
+ with open(log, "w") as logfile:
555
+ try:
556
+ subprocess.run(
557
+ [imp_countries, '--database', dbname],
558
+ env=nenv, stdout=logfile, stderr=logfile)
559
+ except BaseException:
560
+ return -1
561
+ return 0
562
+
563
+ def import_currencies(self, *args):
564
+ """ Import currencies
565
+ """
566
+ dbname = args[0][0]
567
+ log = f"{self.his_base}/log/import_currencies.log"
568
+ imp_currencies = f"{self.his_base}/venv/bin/trytond_import_currencies"
569
+
570
+ """ Load environment variables for this session
571
+ """
572
+ nenv = os.environ.copy()
573
+
574
+ with open(log, "w") as logfile:
575
+ try:
576
+ subprocess.run(
577
+ [imp_currencies, '--database', dbname],
578
+ env=nenv, stdout=logfile, stderr=logfile)
579
+ except BaseException:
580
+ return -1
581
+ return 0
582
+
583
+ def backup(dbname=None):
584
+ """ Makes a backup of the database and attach dir
585
+ """
586
+ pass
587
+
588
+ def command_successful(self, *args):
589
+ return 0
590
+
591
+ def new(self, dbname=None):
592
+ """ Get the instance/db name, admin password and email
593
+ """
594
+ win = curses.newwin(10, 50, 3, 30)
595
+ win.box()
596
+ curses.echo()
597
+ curses.curs_set(1)
598
+ win.addstr(2, 1, "Instance Name:")
599
+ db = win.getstr(2, 20).decode()
600
+ win.addstr(3, 1, "Password:")
601
+ password = win.getstr(3, 20).decode()
602
+ win.addstr(4, 1, "email:")
603
+ email = win.getstr(4, 20).decode()
604
+ win.addstr(6, 1, "Ready? (yes/no)")
605
+ confirm = win.getstr(6, 20).decode()
606
+ if (confirm != "yes"):
607
+ return
608
+
609
+ """
610
+ If we answer yes, go ahead with the instance creation
611
+ """
612
+ curses.curs_set(0)
613
+ win.clear()
614
+ win.refresh()
615
+ logwin = log_window()
616
+ logwin.refresh()
617
+
618
+ """ TASKS is an list of tuples, each element containing
619
+ the method and it's arguments
620
+ """
621
+ TASKS = [('create_db', [db]),
622
+ ('create_instance', [db, email, password]),
623
+ ('install_health_package', [db]),
624
+ ('import_countries', [db]),
625
+ ('import_currencies', [db]),
626
+ ('command_successful', []),
627
+ ]
628
+ row = 0
629
+ for task in TASKS:
630
+ row = row + 1
631
+ tsk = getattr(self, task[0])
632
+ # Pass the string and the actual method as arguments
633
+ rc = task_engine(task=(task, tsk), window=logwin, row=row)
634
+ if rc != 0:
635
+ break
636
+
637
+
638
+ class Help:
639
+
640
+ def display():
641
+ """ show the help window
642
+ """
643
+ pass
644
+
645
+
646
+ class MenuPanel:
647
+ def __init__(self, entries, win, footer, title, leaf=False, action=None):
648
+ curses.start_color()
649
+ # Init color pairs
650
+ curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
651
+ curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED)
652
+ self.window = win
653
+ self.window_title = title
654
+ self.panel = panel.new_panel(self.window)
655
+ self.panel.hide()
656
+ self.footer = footer
657
+ self.footer_panel = panel.new_panel(self.footer)
658
+ self.leaf = leaf
659
+ self.action = action
660
+ panel.update_panels()
661
+
662
+ self.position = 0
663
+ self.entries = entries
664
+
665
+ def centerx(self, msg):
666
+ x = int(curses.COLS / 2) - int(len(msg) / 2)
667
+ return x
668
+
669
+ def menu(self):
670
+ """ Show the menu of the specific section
671
+ """
672
+ self.window.clear()
673
+ self.window.addstr(
674
+ 0, self.centerx(self.window_title), self.window_title)
675
+ self.panel.top()
676
+ self.panel.show()
677
+
678
+ """ Main menu loop"""
679
+ while True:
680
+ self.window.refresh()
681
+ curses.doupdate()
682
+ for index, entry in enumerate(self.entries):
683
+ if index == self.position:
684
+ mode = curses.A_REVERSE
685
+ else:
686
+ mode = curses.A_NORMAL
687
+
688
+ entry_desc = f"{index}: {entry[0]}"
689
+ self.window.addstr(5 + index, 2, entry_desc, mode)
690
+
691
+ self.window.hline(1, 0, curses.ACS_HLINE, curses.COLS)
692
+
693
+ key = self.window.getch()
694
+
695
+ if key == ord("\n"): # Map the Enter key
696
+ # Exec the method associated to the entry
697
+ if not self.leaf:
698
+ if self.entries[self.position][1]:
699
+ self.entries[self.position][1]()
700
+ else:
701
+ self.entries[self.position][1]()
702
+
703
+ if key == ord("q"):
704
+ break
705
+
706
+ if key == ord("h"):
707
+ Help.display()
708
+
709
+ if key == curses.KEY_UP and self.position > 0:
710
+ self.position = self.position - 1
711
+
712
+ if (key == curses.KEY_DOWN
713
+ and self.position < len(self.entries) - 1):
714
+ self.position = self.position + 1
715
+
716
+ if chr(key).isnumeric():
717
+ entry_nr = int(chr(key))
718
+ if entry_nr in range(0, len(self.entries)):
719
+ self.position = int(chr(key))
720
+ else:
721
+ # Out of range
722
+ self.menu_error()
723
+
724
+ self.window.clear()
725
+ self.panel.hide()
726
+ panel.update_panels()
727
+ curses.doupdate()
728
+
729
+ def menu_error(self):
730
+ curses.beep()
731
+
732
+
733
+ class GHControl:
734
+ def __init__(self, stdscr, arguments):
735
+ curses.curs_set(False) # Disable cursor
736
+ basedir = arguments.basedir
737
+
738
+ self.win = self.main_window(stdscr)
739
+ self.footer = self.footer_window(stdscr)
740
+ server = Server(self, arguments=arguments)
741
+ instance = Instance(self, arguments=arguments)
742
+ installation_entries = [
743
+ ("Start Installation", server.setup, arguments),]
744
+ installation_section = MenuPanel(
745
+ installation_entries, self.win, self.footer,
746
+ f"GNU Health HIS Installation (basedir = {basedir})", leaf=True)
747
+
748
+ instance_entries = [
749
+ ("New Instance", instance.new, arguments),]
750
+ instance_section = MenuPanel(
751
+ instance_entries, self.win, self.footer,
752
+ "GNU Health Instance", leaf=True)
753
+
754
+ backup_entries = [("Instance name", Instance.backup),]
755
+ backup_section = MenuPanel(
756
+ backup_entries, self.win, self.footer, "Backup")
757
+
758
+ startstop_entries = [
759
+ ("Start GNU Health HIS", server.start),
760
+ ("Stop Server", ''),
761
+ ]
762
+ startstop_section = MenuPanel(
763
+ startstop_entries, self.win, self.footer,
764
+ "Server start / stop", leaf=True)
765
+
766
+ main_entries = [
767
+ ("Install GNU Health HIS", installation_section.menu),
768
+ ("Create a new DB instance", instance_section.menu),
769
+ ("Start / stop instance", startstop_section.menu),
770
+ ("Update packages / dependencies", ''),
771
+ ("Instance status & logs", ''),
772
+ ("Backup instance", backup_section.menu),
773
+ ]
774
+
775
+ main_section = MenuPanel(
776
+ main_entries, self.win, self.footer,
777
+ f"Welcome to GNU Health Control Center {VERSION}")
778
+
779
+ main_section.menu()
780
+
781
+ def main_window(self, stdscr):
782
+ """ We'll define a main window
783
+ """
784
+ win = curses.newwin(curses.LINES - 5, curses.COLS, 0, 0)
785
+ win.keypad(True) # Need keypad to map KEY_[UP|DOWN]
786
+ return win
787
+
788
+ def footer_window(self, stdscr):
789
+ """ A footer for helpers
790
+ """
791
+ footer = curses.newwin(5, curses.COLS, curses.LINES - 5, 0)
792
+ footer.box()
793
+ server_status = Server.status(self)
794
+
795
+ if server_status:
796
+ attr = curses.color_pair(1) | curses.A_BOLD
797
+ status = "running"
798
+ else:
799
+ attr = curses.color_pair(2) | curses.A_BOLD
800
+ status = "stopped"
801
+
802
+ footer_status = "Server status:"
803
+ footer_hlp = "Press 'q' to go back or exit"
804
+ footer.addstr(
805
+ 1, int(curses.COLS / 2 - len(footer_status + status) / 2),
806
+ footer_status)
807
+
808
+ footer.addstr(
809
+ 1,
810
+ int(
811
+ curses.COLS / 2 - len(footer_status + status) / 2)
812
+ + len(footer_status) + 1, status, attr)
813
+
814
+ footer.addstr(3, int(curses.COLS / 2 - len(footer_hlp) / 2),
815
+ footer_hlp)
816
+ return footer
817
+
818
+
819
+ def cmdline_args():
820
+ parser = argparse.ArgumentParser()
821
+
822
+ parser.add_argument('-B', '--basedir', default='/opt/gnuhealth',
823
+ help="Base directory of installation for "
824
+ "the gnuhealth components\n"
825
+ "default=/opt/gnuhealth.")
826
+
827
+ parser.add_argument('-r', '--release',
828
+ help="GNU Health release (major and minor numbers), "
829
+ "It will affect the installation directory of his, "
830
+ "for example: his-50.")
831
+
832
+ parser.add_argument('-ibr', '--ignore-bashrc', action="store_true",
833
+ help="Does not modify .bashrc, "
834
+ "If users want to install different his versions. "
835
+ "this option can avoid setting conflicts\n "
836
+ "The user should use '/path/to/ghis_env'"
837
+ "for example: "
838
+ "'/opt/gnuhealth/his-50/ghis_env trytond', "
839
+ "'/opt/gnuhealth/his-50/ghis_env trytond-admin', "
840
+ "'/opt/gnuhealth/his-50/ghis_env pip install pandas'")
841
+
842
+ parser.add_argument('-uc', '--update-configs', action="store_true",
843
+ help="Update tryton.cfg, gnuhealthrc, bashrc "
844
+ "configs of an exist installation, this option "
845
+ "is useful to test configs generating for developer.")
846
+
847
+ parser.add_argument('-t', '--test', action="store_true",
848
+ help="Test mode. Uses the repository "
849
+ "test.pypi.org ")
850
+
851
+ parser.add_argument('-p', '--pre', action="store_true",
852
+ help="Use pre-release packages. Usually this flag "
853
+ "is used with --test")
854
+
855
+ return parser.parse_args()
856
+
857
+
858
+ def main():
859
+ arguments = cmdline_args()
860
+ wrapper(GHControl, arguments)
861
+
862
+
863
+ if __name__ == "__main__":
864
+ main()