From 967ccc0d44fc77e3ea622d5e97f467e966ba9ff5 Mon Sep 17 00:00:00 2001 From: Disassembler Date: Mon, 9 Nov 2020 07:52:20 +0100 Subject: [PATCH] Align Sahana Spotter template with the current default template --- .../sahana_data/Spotter/auth_roles.csv | 160 ++-- .../install/sahana_data/Spotter/config.py | 183 +++-- .../{shelter_type.csv => cr_shelter_type.csv} | 0 .../install/sahana_data/Spotter/css.cfg | 3 + .../sahana_data/Spotter/event_type.csv | 4 +- .../sahana_data/Spotter/gis_hierarchy.csv | 12 +- .../sahana_data/Spotter/gis_layer_wms.csv | 2 +- .../install/sahana_data/Spotter/monitor.py | 742 ++++++++++++++++-- .../install/sahana_data/Spotter/parser.py | 133 +++- .../Spotter/supply_person_item_status.csv | 2 + .../install/sahana_data/Spotter/tasks.cfg | 27 +- 11 files changed, 968 insertions(+), 300 deletions(-) rename lxc-apps/sahana/install/sahana_data/Spotter/{shelter_type.csv => cr_shelter_type.csv} (100%) create mode 100644 lxc-apps/sahana/install/sahana_data/Spotter/supply_person_item_status.csv diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/auth_roles.csv b/lxc-apps/sahana/install/sahana_data/Spotter/auth_roles.csv index 2459f06..4022c6a 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/auth_roles.csv +++ b/lxc-apps/sahana/install/sahana_data/Spotter/auth_roles.csv @@ -1,91 +1,91 @@ -uid,role,description,controller,function,table,entity,uacl,oacl,hidden,system,protected,Notes -ANONYMOUS,Anonymous,Unauthenticated users,,,org_organisation,,READ,READ,,,,Required for self-registration -ANONYMOUS,Anonymous,,org,sites_for_org,,,READ,READ,,,,Required for self-registration -ANONYMOUS,Anonymous,,gis,,,,READ,READ,,,, -ANONYMOUS,Anonymous,,vulnerability,,,,READ,READ,,,, -ANONYMOUS,Anonymous,,water,,,,READ,READ,,,, -ANONYMOUS,Anonymous,,delphi,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,Authenticated - all logged-in users,gis,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,,vulnerability,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,,water,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,,delphi,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,,impact,,,,READ,READ,,,, -AUTHENTICATED,Authenticated,,doc,,,,READ,READ,,,, -EDITOR,Editor,Editor - can access & make changes to any unprotected data,pr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,vulnerability,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,survey,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,security,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,fire,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,deploy,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,water,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,budget,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,cap,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,vol,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,delphi,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,org,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,scenario,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,vehicle,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,member,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,cr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,gis,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,asset,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,patient,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,transport,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,po,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,impact,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,dvr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,event,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,msg,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,supply,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,project,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,doc,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,hrm,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,appadmin,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,hms,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,req,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,mpr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,sync,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,inv,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,stats,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,edu,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,dvi,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, -EDITOR,Editor,,cms,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE,,,, +uid,role,description,controller,function,table,entity,uacl,oacl,Notes +ANONYMOUS,Anonymous,Unauthenticated users,,,org_organisation,,READ,READ,Required for self-registration +ANONYMOUS,Anonymous,,org,sites_for_org,,,READ,READ,Required for self-registration +ANONYMOUS,Anonymous,,gis,,,,READ,READ, +ANONYMOUS,Anonymous,,vulnerability,,,,READ,READ, +ANONYMOUS,Anonymous,,water,,,,READ,READ, +ANONYMOUS,Anonymous,,delphi,,,,READ,READ, +AUTHENTICATED,Authenticated,Authenticated - all logged-in users,gis,,,,READ,READ, +AUTHENTICATED,Authenticated,,vulnerability,,,,READ,READ, +AUTHENTICATED,Authenticated,,water,,,,READ,READ, +AUTHENTICATED,Authenticated,,delphi,,,,READ,READ, +AUTHENTICATED,Authenticated,,impact,,,,READ,READ, +AUTHENTICATED,Authenticated,,doc,,,,READ,READ, +EDITOR,Editor,Editor - can access & make changes to any unprotected data,pr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,vulnerability,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,survey,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,security,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,fire,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,deploy,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,water,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,budget,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,cap,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,vol,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,delphi,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,org,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,scenario,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,vehicle,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,member,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,cr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,gis,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,asset,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,patient,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,transport,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,po,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,impact,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,dvr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,event,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,msg,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,supply,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,project,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,doc,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,hrm,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,appadmin,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,hms,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,req,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,mpr,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,sync,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,inv,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,stats,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,edu,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,dvi,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, +EDITOR,Editor,,cms,,,,ALL,CREATE|READ|UPDATE|REVIEW|APPROVE, MAP_ADMIN,Map Admin,"Complementary role: FULL access mode only to the maps and their configurations -When to assign: Staff Directors.",gis,,,,ALL,ALL,,,, -ORG_ADMIN,Organization Admin,,org,,,,ALL,ALL,,,, -ORG_GROUP_ADMIN,Organziation Group Admin,,,,,,,,,,, +When to assign: Staff Directors.",gis,,,,ALL,ALL, +ORG_ADMIN,Organization Admin,,org,,,,ALL,ALL, +ORG_GROUP_ADMIN,Organzation Group Admin,,,,,,,, medical_admin,Medical Details Admin,"Complementary role: FULL access but only to medical data of registered evacuees. It must be assigned with other roles to grant access to the other types of data. -When to assign: Medical Staff.",patient,,,,ALL,ALL,,,, -medical_admin,Medical Details Admin,,hms,,,,ALL,ALL,,,, -medical_admin,Medical Details Admin,,dvi,,,,ALL,ALL,,,, -notification_sender,Notification Sender,,msg,,,,ALL,ALL,,,, +When to assign: Medical Staff.",patient,,,,ALL,ALL, +medical_admin,Medical Details Admin,,hms,,,,ALL,ALL, +medical_admin,Medical Details Admin,,dvi,,,,ALL,ALL, +notification_sender,Notification Sender,,msg,,,,ALL,ALL, org_reader,Organization Reader,"Complementary role: Access in ""READ only"" mode to Organization data and its branches -When to assign: Staff Directors",org,,,,READ,READ,,,, +When to assign: Staff Directors",org,,,,READ,READ, private_user_editor,Private User Editor,"FULL access to modules Evacuees and Shelter. -When to assign: private user member of a shelter / Organization and committed to provide assistance to some evacuees",cr,,,,ALL,ALL,,,, -private_user_editor,Private User Editor,,dvr,,,,ALL,ALL,,,, +When to assign: private user member of a shelter / Organization and committed to provide assistance to some evacuees",cr,,,,ALL,ALL, +private_user_editor,Private User Editor,,dvr,,,,ALL,ALL, private_user_reader,Private User Reader,"Access in ""READ only"" mode to modules Evacuees and Shelter. -When to assign: private user member of a Shelter / Organization and committed to provide assistance to some evacuees.",cr,,,,READ,READ,,,, -private_user_reader,Private User Reader,,dvr,,,,READ,READ,,,, +When to assign: private user member of a Shelter / Organization and committed to provide assistance to some evacuees.",cr,,,,READ,READ, +private_user_reader,Private User Reader,,dvr,,,,READ,READ, public_auth_editor,Public Authority Editor,"FULL access to modules Evacuees and Shelter. -When to assign: supervisor user member of local government office with the rights to register data.",cr,,,,ALL,ALL,,,, -public_auth_editor,Public Authority Editor,,dvr,,,,ALL,ALL,,,, -public_auth_editor,Public Authority Editor,,mpr,,,,READ,READ,,,, +When to assign: supervisor user member of local government office with the rights to register data.",cr,,,,ALL,ALL, +public_auth_editor,Public Authority Editor,,dvr,,,,ALL,ALL, +public_auth_editor,Public Authority Editor,,mpr,,,,READ,READ, public_auth_reader,Public Authority Reader,"Access in ""READ only"" mode to modules Evacuees and Shelter. -When to assign: supervisor user member of a local government office.",dvr,,,,READ,READ,,,, +When to assign: supervisor user member of a local government office.",dvr,,,,READ,READ, staff_admin,Staff Admin,"FULL access to modules Staff, Volunteer, Evacuees and Shelter. -When to assign: Staff assigned to manage one or more shelters.",vol,,,,ALL,ALL,,,, -staff_admin,Staff Admin,,cr,,,,ALL,ALL,,,, -staff_admin,Staff Admin,,dvr,,,,ALL,ALL,,,, -staff_admin,Staff Admin,,hrm,,,,ALL,ALL,,,, +When to assign: Staff assigned to manage one or more shelters.",vol,,,,ALL,ALL, +staff_admin,Staff Admin,,cr,,,,ALL,ALL, +staff_admin,Staff Admin,,dvr,,,,ALL,ALL, +staff_admin,Staff Admin,,hrm,,,,ALL,ALL, staff_reader,Staff Reader,"Access in ""READ only"" mode to modules Staff, Volunteer, Evacuees and Shelter. -When to assign: Staff assigned to manage one or more shelters.",vol,,,,READ,READ,,,, -staff_reader,Staff Reader,,cr,,,,READ,READ,,,, -staff_reader,Staff Reader,,dvr,,,,READ,READ,,,, -staff_reader,Staff Reader,,hrm,,,,READ,READ,,,, +When to assign: Staff assigned to manage one or more shelters.",vol,,,,READ,READ, +staff_reader,Staff Reader,,cr,,,,READ,READ, +staff_reader,Staff Reader,,dvr,,,,READ,READ, +staff_reader,Staff Reader,,hrm,,,,READ,READ, vol_editor,Volunteer Editor,"FULL access to modules Evacuees and Shelter. -When to assign: volunteer (trusted person) volunteer who provides assistance to some evacuees inside a well-defined group of shelters.",cr,,,,ALL,ALL,,,, -vol_editor,Volunteer Editor,,dvr,,,,ALL,ALL,,,, +When to assign: volunteer (trusted person) volunteer who provides assistance to some evacuees inside a well-defined group of shelters.",cr,,,,ALL,ALL, +vol_editor,Volunteer Editor,,dvr,,,,ALL,ALL, vol_reader,Volunteer Reader,"Access in ""READ only"" mode to modules Evacuees and Shelter. -When to assign: volunteer who provides assistance to some evacuees inside a well- defined group of shelters.",cr,,,,READ,READ,,,, -vol_reader,Volunteer Reader,,dvr,,,,READ,READ,,,, +When to assign: volunteer who provides assistance to some evacuees inside a well- defined group of shelters.",cr,,,,READ,READ, +vol_reader,Volunteer Reader,,dvr,,,,READ,READ, diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/config.py b/lxc-apps/sahana/install/sahana_data/Spotter/config.py index 2b84501..8abff19 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/config.py +++ b/lxc-apps/sahana/install/sahana_data/Spotter/config.py @@ -27,6 +27,7 @@ def config(settings): #settings.base.theme = "default" # Enable Guided Tours + # - defaults to module enabled or not #settings.base.guided_tour = True # Authentication settings @@ -128,6 +129,7 @@ def config(settings): # Uncomment this to enable the creation of new locations if a user logs in from an unknown location. Warning: This may lead to many useless location entries #settings.auth.create_unknown_locations = True + # ------------------------------------------------------------------------- # L10n settings # Languages used in the deployment (used for Language Toolbar, GIS Locations, etc) # http://www.loc.gov/standards/iso639-2/php/code_list.php @@ -347,8 +349,8 @@ def config(settings): # 6: Apply Controller, Function, Table ACLs and Entity Realm # 7: Apply Controller, Function, Table ACLs and Entity Realm + Hierarchy # 8: Apply Controller, Function, Table ACLs, Entity Realm + Hierarchy and Delegations - # - settings.security.policy = 7 # Organisation-ACLs + + settings.security.policy = 7 # Organization ACLs # Ownership-rule for records without owner: # True = not owned by any user (strict ownership, default) @@ -787,7 +789,7 @@ def config(settings): # Make Services Hierarchical settings.org.services_hierarchical = True # Set the length of the auto-generated org/site code the default is 10 - #settings.org.site_code_len = 10 + #settings.org.site_code_len = 3 # Set the label for Sites settings.org.site_label = "Facility" # Uncomment to show the date when a Site (Facilities-only for now) was last contacted @@ -936,7 +938,7 @@ def config(settings): settings.inv.recv_shortname = "ARDR" # Types common to both Send and Receive settings.inv.shipment_types = { - 0: T(""), + 0: "", # current.messages["NONE"] but current.messages is defined only after 000_config.py is executed 1: T("Other Warehouse"), 2: T("Donation"), 3: T("Foreign Donation"), @@ -952,7 +954,7 @@ def config(settings): # 34: T("Purchase"), # } #settings.inv.item_status = { - # 0: current.messages["NONE"], + # 0: "", # current.messages["NONE"] but current.messages is defined only after 000_config.py is executed # 1: T("Dump"), # 2: T("Sale"), # 3: T("Reject"), @@ -1168,9 +1170,8 @@ def config(settings): settings.modules = OrderedDict([ # Core modules which shouldn't be disabled ("default", Storage( - name_nice = T("Home"), + name_nice = T("Default"), restricted = False, # Use ACLs to control access to this module - access = None, # All Users (inc Anonymous) can see this module in the default menu & access the controller module_type = None # This item is not shown in the menu )), ("admin", Storage( @@ -1197,7 +1198,7 @@ def config(settings): #description = "WebSetup", restricted = True, access = "|1|", # Only Administrators can see this module in the default menu & access the controller - module_type = None # No Menu + module_type = None # No Menu )), ("sync", Storage( name_nice = T("Synchronization"), @@ -1211,7 +1212,7 @@ def config(settings): module_type = None, )), ("translate", Storage( - name_nice = T("Translation Functionality"), + name_nice = T("Translation"), #description = "Selective translation of strings based on module.", module_type = None, )), @@ -1279,11 +1280,10 @@ def config(settings): module_type = 4 )), #("proc", Storage( - # name_nice = T("Procurement"), - # #description = "Ordering & Purchasing of Goods & Services", - # restricted = True, - # module_type = 10 - # )), + # name_nice = T("Procurement"), + # #description = "Ordering & Purchasing of Goods & Services", + # module_type = 10 + #)), ("asset", Storage( name_nice = T("Assets"), #description = "Recording and Assigning Assets", @@ -1309,47 +1309,11 @@ def config(settings): restricted = True, module_type = 2 )), - #("survey", Storage( - # name_nice = T("Surveys"), - # #description = "Create, enter, and manage surveys.", - # restricted = True, - # module_type = 5, - #)), - ("dc", Storage( - name_nice = T("Assessments"), - #description = "Data collection tool", - restricted = True, - module_type = 5 - )), - ("cr", Storage( - name_nice = T("Shelters"), - #description = "Tracks the location, capacity and breakdown of victims in Shelters", + ("stats", Storage( + name_nice = T("Statistics"), + #description = "Manages statistics", restricted = True, - module_type = 9 - )), - ("hms", Storage( - name_nice = T("Hospitals"), - #description = "Helps to monitor status of hospitals", - restricted = True, - module_type = 9 - )), - #("disease", Storage( - # name_nice = T("Disease Tracking"), - # #description = "Helps to track cases and trace contacts in disease outbreaks", - # restricted = True, - # module_type = 10 - #)), - #("br", Storage( - # name_nice = T("Beneficiary Registry"), - # #description = "Beneficiary Registry and Case Management", - # restricted = True, - # module_type = 10, - #)), - ("dvr", Storage( - name_nice = T("Disaster Victim Registry"), - #description = "Allow affected individuals & households to register to receive compensation and distributions", - restricted = True, - module_type = 10, + module_type = None, )), ("event", Storage( name_nice = T("Events"), @@ -1357,16 +1321,29 @@ def config(settings): restricted = True, module_type = 10, )), - ("transport", Storage( - name_nice = T("Transport"), + ("br", Storage( + name_nice = T("Beneficiary Registry"), + #description = "Allow affected individuals & households to register to receive compensation and distributions", restricted = True, module_type = 10, )), - ("stats", Storage( - name_nice = T("Statistics"), - #description = "Manages statistics", + ("cr", Storage( + name_nice = T("Shelters"), + #description = "Tracks the location, capacity and breakdown of victims in Shelters", restricted = True, - module_type = None, + module_type = 9 + )), + ("dc", Storage( + name_nice = T("Assessments"), + #description = "Data collection tool", + restricted = True, + module_type = 5 + )), + ("hms", Storage( + name_nice = T("Hospitals"), + #description = "Helps to monitor status of hospitals", + restricted = True, + module_type = 9 )), ("member", Storage( name_nice = T("Members"), @@ -1386,45 +1363,28 @@ def config(settings): restricted = True, module_type = 10, )), - # Deprecated: Replaced by event - #("irs", Storage( - # name_nice = T("Incidents"), - # #description = "Incident Reporting System", - # restricted = True, + #("disease", Storage( + # name_nice = T("Disease Tracking"), + # #description = "Helps to track cases and trace contacts in disease outbreaks", # module_type = 10 #)), - ("dvi", Storage( - name_nice = T("Disaster Victim Identification"), - #description = "Disaster Victim Identification", - restricted = True, - module_type = 10, - #access = "|DVI|", # Only users with the DVI role can see this module in the default menu & access the controller - )), ("edu", Storage( name_nice = T("Schools"), #description = "Helps to monitor status of schools", restricted = True, module_type = 10 )), - ("mpr", Storage( - name_nice = T("Missing Person Registry"), - #description = "Helps to report and search for missing persons", - restricted = True, - module_type = 10, - )), - # https://github.com/sahana/eden/issues/1562 - #("vulnerability", Storage( - # name_nice = T("Vulnerability"), - # #description = "Manages vulnerability indicators", - # restricted = True, - # module_type = 10, - #)), ("fire", Storage( name_nice = T("Fire Stations"), #description = "Fire Station Management", restricted = True, module_type = 1, )), + ("transport", Storage( + name_nice = T("Transport"), + restricted = True, + module_type = 10, + )), ("water", Storage( name_nice = T("Water"), #description = "Flood Gauges show water levels in various parts of the country", @@ -1449,6 +1409,36 @@ def config(settings): restricted = True, module_type = 10, )), + # Vulnerability temporarily disabled - https://github.com/sahana/eden/issues/1562 + #("vulnerability", Storage( + # name_nice = T("Vulnerability"), + # #description = "Manages vulnerability indicators", + # module_type = 10, + # )), + ("work", Storage( + name_nice = T("Jobs"), + #description = "Simple Volunteer Jobs Management", + restricted = False, + module_type = None, + )), + # Deprecated: Replaced by BR + #("dvr", Storage( + # name_nice = T("Beneficiary Registry"), + # #description = "Disaster Victim Registry", + # module_type = 10 + #)), + # Deprecated: Replaced by event + #("irs", Storage( + # name_nice = T("Incidents"), + # #description = "Incident Reporting System", + # module_type = 10 + #)), + # Deprecated: Replaced by DC + #("survey", Storage( + # name_nice = T("Surveys"), + # #description = "Create, enter, and manage surveys.", + # module_type = 5, + #)), # These are specialist modules ("cap", Storage( name_nice = T("CAP"), @@ -1456,11 +1446,23 @@ def config(settings): restricted = True, module_type = 10, )), + ("dvi", Storage( + name_nice = T("Disaster Victim Identification"), + #description = "Disaster Victim Identification", + restricted = True, + module_type = 10, + #access = "|DVI|", # Only users with the DVI role can see this module in the default menu & access the controller + )), + ("mpr", Storage( + name_nice = T("Missing Person Registry"), + #description = "Helps to report and search for missing persons", + restricted = True, + module_type = 10, + )), # Requires RPy2 & PostgreSQL #("climate", Storage( # name_nice = T("Climate"), # #description = "Climate data portal", - # restricted = True, # module_type = 10, #)), ("delphi", Storage( @@ -1473,7 +1475,6 @@ def config(settings): #("building", Storage( # name_nice = T("Building Assessments"), # #description = "Building Safety Assessments", - # restricted = True, # module_type = 10, #)), # Deprecated by Surveys module @@ -1481,13 +1482,11 @@ def config(settings): #("assess", Storage( # name_nice = T("Assessments"), # #description = "Rapid Assessments & Flexible Impact Assessments", - # restricted = True, # module_type = 10, #)), #("impact", Storage( # name_nice = T("Impacts"), # #description = "Used by Assess", - # restricted = True, # module_type = None, #)), #("ocr", Storage( @@ -1496,12 +1495,6 @@ def config(settings): # restricted = False, # module_type = None, #)), - ("work", Storage( - name_nice = T("Jobs"), - #description = "Simple Volunteer Jobs Management", - restricted = False, - module_type = None, - )), ]) # END ========================================================================= diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/shelter_type.csv b/lxc-apps/sahana/install/sahana_data/Spotter/cr_shelter_type.csv similarity index 100% rename from lxc-apps/sahana/install/sahana_data/Spotter/shelter_type.csv rename to lxc-apps/sahana/install/sahana_data/Spotter/cr_shelter_type.csv diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/css.cfg b/lxc-apps/sahana/install/sahana_data/Spotter/css.cfg index 085b141..d4e4800 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/css.cfg +++ b/lxc-apps/sahana/install/sahana_data/Spotter/css.cfg @@ -13,6 +13,8 @@ plugins/jquery.tagit.css ui/core.css ui/autocomplete.css ui/button.css +#ui/checkboxradio.css +#ui/controlgroup.css ui/datepicker.css ui/dialog.css # Needed for Delphi @@ -29,6 +31,7 @@ ui/tabs.css ui/fgtimepicker.css ui/multiselect.css ui/timepicker-addon.css +#calendars/ui.calendars.picker.css #calendars/ui-smoothness.calendars.picker.css ../themes/foundation/jquery-ui.theme.css gis/style.css diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/event_type.csv b/lxc-apps/sahana/install/sahana_data/Spotter/event_type.csv index 5786334..f5cb38d 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/event_type.csv +++ b/lxc-apps/sahana/install/sahana_data/Spotter/event_type.csv @@ -10,8 +10,8 @@ Epidemic,, Explosion,, Extreme Winter Conditions,, Fire,, -Flash Floods,, -Floods,, +Flash Flood,, +Flood,, Food Security,, Heat Wave,, Land Slide,, diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/gis_hierarchy.csv b/lxc-apps/sahana/install/sahana_data/Spotter/gis_hierarchy.csv index a75485b..db269a1 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/gis_hierarchy.csv +++ b/lxc-apps/sahana/install/sahana_data/Spotter/gis_hierarchy.csv @@ -26,7 +26,7 @@ SITE_DEFAULT,,"State / Province","County / District","City / Town / Village","Vi ,CO,Department,Municipality,Corregimiento, ,CR,Province,Canton,District, ,CU,Province,Municipality, -,DE,"Federal State","Rural District / District","Town / Municipality",Village, +,DE,"Federal State","Rural District / District","Town / Municipality",Locality, ,DO,Province,Municipality,,,, ,EC,Province,Canton,Parish,,, ,FI,Regional State Administrative Agency,Region,Sub-region,Municipality,, @@ -44,12 +44,14 @@ SITE_DEFAULT,,"State / Province","County / District","City / Town / Village","Vi ,IN,State,District,Sub-District,,, ,IQ,Province,District,,,, ,IT,Region,Province,Commune,,, +,JO,Governorate,District,Subdistrict,,, ,KE,County,Constituency,Location,SubLocation, ,KG,"Oblast / State City","District (Rayon) / Oblast City","Town / Village Group",Village,, ,KH,Province,District,Commune,Village, ,KI,Island Council,,,, ,KZ,Province,District,,,, ,LA,Province,District,Village,,, +,LB,Governorate,District,,,, ,LK,Province,District,Divisional Secretariat,Grama Niladhari,, ,LR,County,District,Clan,,, ,LT,County,Municipality,Eldership,,, @@ -70,6 +72,8 @@ SITE_DEFAULT,,"State / Province","County / District","City / Town / Village","Vi ,PG,Province,District,LLG,Village,, ,PH,Region,Province,"City / Municipality",Barangay,, ,PK,Province,District,Tehsil,Union Council,Village, +,PL,Province,County,Municipality,,, +,PR,Municipality,Barrio,,,, ,PY,Department,District,"City / Town / Village",,, ,RS,District,"Municipality / City",,,, ,SB,Province,Ward,, @@ -77,18 +81,18 @@ SITE_DEFAULT,,"State / Province","County / District","City / Town / Village","Vi ,SG,Region,,, ,SL,Province,District,Chiefdom,,, ,SV,Department,Municipality, +,SY,Governorate,District,Subdistrict ,TD,Region,Department,Sub-Prefecture,Canton, ,TH,Province,District,Sub-District,,, ,TJ,Province,District,Jamoat,Village,, ,TL,District,SubDistrict,Suco,Aldeia,, ,TM,Province,District,,,, ,TO,Island Group,District,,,, -,TR,City,Town,District,,, +,TR,Province,District / Town,Neighborhood / Village,,, ,TV,Island Council,,,, -,PR,Municipality,Barrio,,,, ,US,State,County,City,Neighborhood,,False ,UZ,Province,District,,,, -,VU,Province,Area Council,, ,VN,Province,District,Commune,,, +,VU,Province,Area Council,, ,XK,District,Municipality,,,, ,YE,Governorate,District,Sub-District,Village,, diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/gis_layer_wms.csv b/lxc-apps/sahana/install/sahana_data/Spotter/gis_layer_wms.csv index 9dfbde4..f753436 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/gis_layer_wms.csv +++ b/lxc-apps/sahana/install/sahana_data/Spotter/gis_layer_wms.csv @@ -12,4 +12,4 @@ Population Density 2010 (Alternate server),GPWv3,http://sedac.ciesin.columbia.ed Population Density 2000 (Persons per km2),GRUMPv1,http://beta.sedac.ciesin.columbia.edu:8080/geoserver/wms?,grump-v1:grump-v1-population-density_2000 ,Population,WGS84,True,False,False,True,0.8,image/png,False,,, Precipitation forecast,,http://geo.weatheroffice.gc.ca/geomet/?,GDPS.ETA_PR,Weather,,True,False,False,True,0.4,image/png,False,http://geo.weatheroffice.gc.ca/geomet/?LANG=E%26SERVICE=WMS%26VERSION=1.1.1%26REQUEST=GetLegendGraphic%26STYLE=PRECIPMM%26LAYER=GDPS.ETA_PR%26format=image/png,PRECIPMM, Cloud forecast,,http://geo.weatheroffice.gc.ca/geomet/?,GDPS.ETA_NT,Weather,,True,False,False,True,0.4,image/png,False,http://geo.weatheroffice.gc.ca/geomet/?LANG=E%26SERVICE=WMS%26VERSION=1.1.1%26REQUEST=GetLegendGraphic%26STYLE=CLOUD%26LAYER=GDPS.ETA_NT%26format=image/png,CLOUD, -Sea Level: Rise of 2m,Data from https://www.cresis.ku.edu/data/sea-level-rise-maps,http://lacrmt.sahanafoundation.org:8080/geoserver/wms?,lacrmt:inund2,Hazards,,True,False,False,True,0.4,image/png,False,,, +Sea Level: Rise of 2m,Data from https://www.cresis.ku.edu/data/sea-level-rise-maps,http://lacrmt.sahanafoundation.org:8080/geoserver/wms?,lacrmt:inund2,Hazards,,False,False,False,True,0.4,image/png,False,,, diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/monitor.py b/lxc-apps/sahana/install/sahana_data/Spotter/monitor.py index a2d05c0..9780e25 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/monitor.py +++ b/lxc-apps/sahana/install/sahana_data/Spotter/monitor.py @@ -5,7 +5,7 @@ Template-specific Monitoring Tasks are defined here. - @copyright: 2014-2019 (c) Sahana Software Foundation + @copyright: 2014-2020 (c) Sahana Software Foundation @license: MIT Permission is hereby granted, free of charge, to any person @@ -33,11 +33,26 @@ __all__ = ("S3Monitor",) import datetime +import json +import os import platform import subprocess +import sys from gluon import current -#from gluon.tools import fetch + +try: + import requests +except ImportError: + REQUESTS = None +else: + REQUESTS = True + +INSTANCE_TYPES = {"prod": 1, + "setup": 2, + "test": 3, + "demo": 4, + } # ============================================================================= class S3Monitor(object): @@ -45,11 +60,366 @@ class S3Monitor(object): Monitoring Check Scripts """ + # ------------------------------------------------------------------------- + @staticmethod + def diskspace(task_id, run_id): + """ + Test the free diskspace + """ + + db = current.db + s3db = current.s3db + + # Read the Task Options + ttable = s3db.setup_monitor_task + task = db(ttable.id == task_id).select(ttable.options, + ttable.server_id, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get + + partition = options_get("partition", "/") # Root Partition by default + space_min = options_get("space_min", 1000000000) # 1 Gb + + stable = s3db.setup_server + server = db(stable.id == task.server_id).select(stable.host_ip, + stable.remote_user, + stable.private_key, + limitby = (0, 1) + ).first() + + if server.host_ip == "127.0.0.1": + result = os.statvfs(partition) + space = result.f_bavail * result.f_frsize + percent = float(result.f_bavail) / float(result.f_blocks) * 100 + if space < space_min: + return {"result": "Warning: %s free (%d%%)" % \ + (_bytes_to_size_string(space), percent), + "status": 2, + } + + return {"result": "OK. %s free (%d%%)" % \ + (_bytes_to_size_string(space), percent), + "status": 1, + } + + ssh = _ssh(server) + if isinstance(ssh, dict): + # We failed to login + return ssh + + command = "import os;result=os.statvfs('%s');print(result.f_bavail);print(result.f_frsize);print(result.f_blocks)" % partition + stdin, stdout, stderr = ssh.exec_command('python -c "%s"' % command) + outlines = stdout.readlines() + ssh.close() + + f_bavail = int(outlines[0]) + f_frsize = int(outlines[1]) + f_blocks = int(outlines[2]) + + space = f_bavail * f_frsize + percent = float(f_bavail) / float(f_blocks) * 100 + if space < space_min: + return {"result": "Warning: %s free (%d%%)" % \ + (_bytes_to_size_string(space), percent), + "status": 2, + } + + return {"result": "OK. %s free (%d%%)" % \ + (_bytes_to_size_string(space), percent), + "status": 1, + } + + # ------------------------------------------------------------------------- + @staticmethod + def eden(task_id, run_id): + """ + Test that we can retrieve the public_url, which checks: + - DNS must resolve to correct IP + - Server must be up + - Firewall cannot be blocking + - Web server is running + - UWSGI is running + - Database is running + - Eden can connect to Database + """ + + if REQUESTS is None: + return {"result": "Critical: Requests library not installed", + "status": 3, + } + + db = current.db + s3db = current.s3db + + # Read the Task Options + ttable = s3db.setup_monitor_task + task = db(ttable.id == task_id).select(ttable.options, + ttable.deployment_id, + ttable.server_id, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get + + appname = options_get("appname", "eden") + public_url = options_get("public_url") + timeout = options_get("timeout", 60) # 60s (default is no timeout!) + + if not public_url: + deployment_id = task.deployment_id + if deployment_id: + # Read from the instance + itable = s3db.setup_instance + query = (itable.deployment_id == deployment_id) & \ + (itable.type == 1) + instance = db(query).select(itable.url, + limitby = (0, 1) + ).first() + if instance: + public_url = instance.url + if not public_url: + # Use the server name + stable = s3db.setup_server + server = db(stable.id == task.server_id).select(stable.name, + limitby = (0, 1) + ).first() + public_url = "https://%s" % server.name + + url = "%(public_url)s/%(appname)s/default/public_url" % {"appname": appname, + "public_url": public_url, + } + + try: + r = requests.get(url, timeout = timeout) # verify=True + except requests.exceptions.SSLError: + # e.g. Expired Certificate + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical: SSL Error\n\n%s" % tb_text, + "status": 3, + } + except requests.exceptions.Timeout: + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical: Timeout Error\n\n%s" % tb_text, + "status": 3, + } + except requests.exceptions.TooManyRedirects: + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical: TooManyRedirects Error\n\n%s" % tb_text, + "status": 3, + } + except requests.exceptions.ConnectionError: + # e.g. DNS Error + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical: Connection Error\n\n%s" % tb_text, + "status": 3, + } + + if r.status_code != 200: + return {"result": "Critical: HTTP Error. Status = %s" % r.status_code, + "status": 3, + } + + if r.text != public_url: + return {"result": "Critical: Page returned '%s' instead of '%s'" % \ + (r.text, public_url), + "status": 3, + } + + latency = int(r.elapsed.microseconds / 1000) + latency_max = options_get("latency_max", 2000) # 2 seconds + if latency > latency_max: + return {"result": "Warning: Latency of %s exceeded threshold of %s." % \ + (latency, latency_max), + "status": 2, + } + + return {"result": "OK. Latency: %s" % latency, + "status": 1, + } + + # ------------------------------------------------------------------------- + @staticmethod + def email_round_trip(task_id, run_id): + """ + Check that a Mailbox is being Polled & Parsed OK and can send replies + """ + + # Read the Task Options + ttable = current.s3db.setup_monitor_task + task = current.db(ttable.id == task_id).select(ttable.options, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get + + to = options_get("to", None) + if not to: + return {"result": "Critical: No recipient address specified", + "status": 3, + } + + subject = options_get("subject", "") + message = options_get("message", "") + reply_to = options_get("reply_to") + if not reply_to: + # Use the outbound email address + reply_to = current.deployment_settings.get_mail_sender() + if not reply_to: + return {"result": "Critical: No reply_to specified", + "status": 3, + } + + # Append the run_id for the remote parser to identify as a monitoring message & return to us to be able to match the run + message = "%s\n%s" % (message, ":run_id:%s:" % run_id) + + # Append the reply_to address for the remote parser + message = "%s\n%s" % (message, ":reply_to:%s:" % reply_to) + + # Send the Email + result = current.msg.send_email(to, + subject, + message, + reply_to = reply_to) + + if result: + # Schedule a task to see if the reply has arrived + wait = options_get("wait", 60) # Default = 60 minutes + start_time = datetime.datetime.utcnow() + \ + datetime.timedelta(minutes = wait) + current.s3task.schedule_task("setup_monitor_check_email_reply", + args = [run_id], + start_time = start_time, + timeout = 300, # seconds + repeats = 1 # one-time + ) + return {"result": "OK so far: Waiting for Reply", + "status": 1, + } + + return {"result": "Critical: Unable to send Email", + "status": 3, + } + + # ------------------------------------------------------------------------- + @staticmethod + def http(task_id, run_id): + """ + Test that HTTP is accessible + """ + + if REQUESTS is None: + return {"result": "Critical: Requests library not installed", + "status": 3, + } + + raise NotImplementedError + + # ------------------------------------------------------------------------- + @staticmethod + def https(task_id, run_id): + """ + Test that HTTP is accessible + """ + + if REQUESTS is None: + return {"result": "Critical: Requests library not installed", + "status": 3, + } + + raise NotImplementedError + + # ------------------------------------------------------------------------- + @staticmethod + def load_average(task_id, run_id): + """ + Test the Load Average + """ + + db = current.db + s3db = current.s3db + + # Read the Task Options + ttable = s3db.setup_monitor_task + task = db(ttable.id == task_id).select(ttable.options, + ttable.server_id, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get + + which = options_get("which", 2) # 15 min average + load_max = options_get("load_max", 2) + + stable = s3db.setup_server + server = db(stable.id == task.server_id).select(stable.host_ip, + stable.remote_user, + stable.private_key, + limitby = (0, 1) + ).first() + + if server.host_ip == "127.0.0.1": + loadavg = os.getloadavg() + if loadavg[which] > load_max: + return {"result": "Warning: load average: %0.2f, %0.2f, %0.2f" % \ + (loadavg[0], loadavg[1], loadavg[2]), + "status": 2, + } + + return {"result": "OK. load average: %0.2f, %0.2f, %0.2f" % \ + (loadavg[0], loadavg[1], loadavg[2]), + "status": 1, + } + + ssh = _ssh(server) + if isinstance(ssh, dict): + # We failed to login + return ssh + + command = "import os;loadavg=os.getloadavg();print(loadavg[0]);print(loadavg[1]);print(loadavg[2])" + stdin, stdout, stderr = ssh.exec_command('python -c "%s"' % command) + outlines = stdout.readlines() + ssh.close() + + loadavg = {0: float(outlines[0]), + 1: float(outlines[1]), + 2: float(outlines[2]), + } + + if loadavg[which] > load_max: + return {"result": "Warning: load average: %0.2f, %0.2f, %0.2f" % \ + (loadavg[0], loadavg[1], loadavg[2]), + "status": 2, + } + + return {"result": "OK. load average: %0.2f, %0.2f, %0.2f" % \ + (loadavg[0], loadavg[1], loadavg[2]), + "status": 1, + } + # ------------------------------------------------------------------------- @staticmethod def ping(task_id, run_id): """ - Ping a server + ICMP Ping a server + - NB AWS instances don't respond to ICMP Ping by default, but this can be enabled in the Firewall """ s3db = current.s3db @@ -66,83 +436,323 @@ class S3Monitor(object): host_ip = row.host_ip try: - output = subprocess.check_output("ping -{} 1 {}".format("n" if platform.system().lower == "windows" else "c", host_ip), shell=True) - except Exception as e: - # Critical: Ping failed - return 3 - else: - # OK - return 1 + # @ToDo: Replace with socket? + # - we want to see the latency + if platform.system().lower == "windows": + _format = "n" + else: + _format = "c" + output = subprocess.check_output("ping -{} 1 {}".format(_format, + host_ip), + shell = True) + except Exception: + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical: Ping failed\n\n%s" % tb_text, + "status": 3, + } - # ------------------------------------------------------------------------- - #@staticmethod - #def http(task_id, run_id): - # """ - # Test that HTTP is accessible - # """ - - # ------------------------------------------------------------------------- - #@staticmethod - #def https(task_id, run_id): - # """ - # Test that HTTPS is accessible - # @ToDo: Check that SSL certificate hasn't expired - # """ + return {"result": "OK", + "status": 1, + } # ------------------------------------------------------------------------- @staticmethod - def email_round_trip(task_id, run_id): + def scheduler(task_id, run_id): """ - Check that a Mailbox is being Polled & Parsed OK and can send replies + Test whether the scheduler is running """ + db = current.db + s3db = current.s3db + # Read the Task Options - otable = current.s3db.setup_monitor_task_option - query = (otable.task_id == task_id) & \ - (otable.deleted == False) - rows = current.db(query).select(otable.tag, - otable.value, - ) - options = dict((row.tag, row.value) for row in rows) + ttable = s3db.setup_monitor_task + task = db(ttable.id == task_id).select(ttable.options, + ttable.server_id, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get - to = options.get("to", None) - if not to: - return False + stable = s3db.setup_server + server = db(stable.id == task.server_id).select(stable.host_ip, + stable.remote_user, + stable.private_key, + limitby = (0, 1) + ).first() - subject = options.get("subject", "") - message = options.get("message", "") - reply_to = options.get("reply_to") - if not reply_to: - # Use the outbound email address - reply_to = current.deployment_settings.get_mail_sender() - if not reply_to: - return False + earliest = current.request.utcnow - datetime.timedelta(seconds = 900) # 15 minutes - # Append the run_id for the remote parser to identify as a monitoring message & return to us to be able to match the run - message = "%s\n%s" % (message, ":run_id:%s:" % run_id) + if server.host_ip == "127.0.0.1": + # This shouldn't make much sense as a check, since this won't run if the scheduler has died + # - however in practise, it can actually provide useful warning! - # Append the reply_to address for the remote parser - message = "%s\n%s" % (message, ":reply_to:%s:" % reply_to) + wtable = s3db.scheduler_worker + worker = db(wtable.status == "ACTIVE").select(wtable.last_heartbeat, + limitby = (0, 1) + ).first() - # Send the Email - result = current.msg.send_email(to, - subject, - message, - reply_to=reply_to) + error = None + if worker is None: + error = "Warning: Scheduler not ACTIVE" - if result: - # Schedule a task to see if the reply has arrived after 1 hour - start_time = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - current.s3task.schedule_task("setup_monitor_check_email_reply", - args = [run_id], - start_time = start_time, - timeout = 300, # seconds - repeats = 1 # one-time - ) - # Warning: No reply received yet - return 2 + elif worker.last_heartbeat < earliest: + error = "Warning: Scheduler stalled since %s" % worker.last_heartbeat.strftime("%H:%M %a %d %b") + + if error: + appname = options_get("appname", "eden") + instance = options_get("instance", "prod") + + # Restart uwsgi + error += "\n\nAttempting to restart:\n" + # Note this needs to actually run after last task as it kills us ;) + # NB Need to ensure the web2py user has permission to run sudo + command = 'echo "sudo service uwsgi-%s restart" | at now + 1 minutes' % instance + output = subprocess.check_output(command, + stderr = subprocess.STDOUT, + shell = True) + error += output.decode("utf-8") + # Restart Monitoring Scripts + command = 'echo "cd /home/%s;python web2py.py --no-banner -S %s -M -R applications/%s/static/scripts/tools/restart_monitor_tasks.py" | at now + 5 minutes' % \ + (instance, appname, appname) + output = subprocess.check_output(command, + stderr = subprocess.STDOUT, + shell = True) + error += output.decode("utf-8") + return {"result": error, + "status": 3, + } + + return {"result": "OK", + "status": 1, + } + + + ssh = _ssh(server) + if isinstance(ssh, dict): + # We failed to login + return ssh + + appname = options_get("appname", "eden") + instance = options_get("instance", "prod") + + command = "cd /home/%s;python web2py.py --no-banner -S %s -M -R applications/%s/static/scripts/tools/check_scheduler.py -A '%s'" % \ + (instance, appname, appname, earliest) + stdin, stdout, stderr = ssh.exec_command(command) + outlines = stdout.readlines() + + if outlines: + error = outlines[0] + # Restart uwsgi + error += "\n\nAttempting to restart:\n" + command = "sudo service uwsgi-%s restart" % instance + stdin, stdout, stderr = ssh.exec_command(command) + outlines = stdout.readlines() + if outlines: + error += "\n".join(outlines) + else: + # Doesn't usually give any output + error += "OK" + ssh.close() + return {"result": error, + "status": 3, + } + ssh.close() + + return {"result": "OK", + "status": 1, + } + + # ------------------------------------------------------------------------- + @staticmethod + def tcp(task_id, run_id): + """ + Test that a TCP port is accessible + """ + + raise NotImplementedError + + # ------------------------------------------------------------------------- + @staticmethod + def tickets(task_id, run_id): + """ + Test whether there are new tickets today + - designed to be run daily (period 86400s) + """ + + db = current.db + s3db = current.s3db + + # Read the Task Options + ttable = s3db.setup_monitor_task + task = db(ttable.id == task_id).select(ttable.options, + #ttable.period, + ttable.server_id, + limitby = (0, 1) + ).first() + options = task.options or {} + options_get = options.get + + stable = s3db.setup_server + server = db(stable.id == task.server_id).select(stable.host_ip, + stable.remote_user, + stable.private_key, + stable.deployment_id, + limitby = (0, 1) + ).first() + + request = current.request + today = request.utcnow.date().isoformat() + + if server.host_ip == "127.0.0.1": + appname = request.application + public_url = current.deployment_settings.get_base_public_url() + tickets = os.listdir("applications/%s/errors" % appname) + new = [] + for ticket in tickets: + #if os.stat(ticket).st_mtime < now - task.period: + if today in ticket: + url = "%s/%s/admin/ticket/%s/%s" % (public_url, + appname, + appname, + ticket, + ) + new.append(url) + + if new: + return {"result": "Warning: New tickets:\n\n%s" % "\n".join(new), + "status": 2, + } + + return {"result": "OK", + "status": 1, + } + + ssh = _ssh(server) + if isinstance(ssh, dict): + # We failed to login + return ssh + + appname = options_get("appname", "eden") + instance = options_get("instance", "prod") + + command = "import os;ts=os.listdir('/home/%s/applications/%s/errors');for t in ts:print(t) if '%s' in t" % \ + (instance, appname, today) + stdin, stdout, stderr = ssh.exec_command('python -c "%s"' % command) + outlines = stdout.readlines() + ssh.close() + + if outlines: + itable = s3db.setup_instance + query = (itable.deployment_id == server.deployment_id) & \ + (itable.type == INSTANCE_TYPES[instance]) + instance = db(query).select(itable.url, + limitby = (0, 1) + ).first() + public_url = instance.url + new = [] + for ticket in outlines: + url = "%s/%s/admin/ticket/%s/%s" % (public_url, + appname, + appname, + ticket, + ) + new.append(url) + return {"result": "Warning: New tickets:\n\n%s" % "\n".join(new), + "status": 2, + } + + return {"result": "OK", + "status": 1, + } + +# ============================================================================= +def _bytes_to_size_string(b): +#def _bytes_to_size_string(b: int) -> str: + """ + Convert a number in bytes to a sensible unit. + + From https://github.com/jamesoff/simplemonitor/blob/develop/simplemonitor/Monitors/host.py#L35 + """ + + kb = 1024 + mb = kb * 1024 + gb = mb * 1024 + tb = gb * 1024 + + if b > tb: + return "%0.2fTiB" % (b / float(tb)) + elif b > gb: + return "%0.2fGiB" % (b / float(gb)) + elif b > mb: + return "%0.2fMiB" % (b / float(mb)) + elif b > kb: + return "%0.2fKiB" % (b / float(kb)) + else: + return str(b) + +# ============================================================================= +def _ssh(server): + """ + SSH into a Server + """ + + remote_user = server.remote_user + private_key = server.private_key + if not private_key or not remote_user: + if remote_user: + return {"result": "Critical. Missing Private Key", + "status": 3, + } + elif private_key: + return {"result": "Critical. Missing Remote User", + "status": 3, + } else: - # Critical: Unable to send Email - return 3 + return {"result": "Critical. Missing Remote User & Private Key", + "status": 3, + } + + # SSH in & run check + try: + import paramiko + except ImportError: + return {"result": "Critical. Paramiko required.", + "status": 3, + } + + keyfile = open(os.path.join(current.request.folder, "uploads", private_key), "r") + mykey = paramiko.RSAKey.from_private_key(keyfile) + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + ssh.connect(hostname = server.host_ip, + username = remote_user, + pkey = mykey) + except paramiko.ssh_exception.AuthenticationException: + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical. Authentication Error\n\n%s" % tb_text, + "status": 3, + } + except paramiko.ssh_exception.SSHException: + import traceback + tb_parts = sys.exc_info() + tb_text = "".join(traceback.format_exception(tb_parts[0], + tb_parts[1], + tb_parts[2])) + return {"result": "Critical. SSH Error\n\n%s" % tb_text, + "status": 3, + } + + return ssh # END ========================================================================= diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/parser.py b/lxc-apps/sahana/install/sahana_data/Spotter/parser.py index b09bd31..b3c73a8 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/parser.py +++ b/lxc-apps/sahana/install/sahana_data/Spotter/parser.py @@ -5,7 +5,7 @@ Template-specific Message Parsers are defined here. - @copyright: 2012-2019 (c) Sahana Software Foundation + @copyright: 2012-2020 (c) Sahana Software Foundation @license: MIT Permission is hereby granted, free of charge, to any person @@ -55,6 +55,53 @@ class S3Parser(object): Message Parsing Template """ + # ------------------------------------------------------------------------- + @staticmethod + def parse_email(message): + """ + Parse Responses + - parse responses to mails from the Monitor service + """ + + reply = None + + db = current.db + s3db = current.s3db + + # Need to use Raw currently as not showing in Body + message_id = message.message_id + table = s3db.msg_email + record = db(table.message_id == message_id).select(table.raw, + limitby=(0, 1) + ).first() + if not record: + return reply + + message_body = record.raw + if not message_body: + return reply + + # What type of message is this? + if ":run_id:" in message_body: + # Response to Monitor Check + + # Parse Mail + try: + run_id = S3Parser._parse_value(message_body, "run_id") + run_id = int(run_id) + except: + return reply + + # Update the Run entry to show that we have received the reply OK + rtable = s3db.monitor_run + db(rtable.id == run_id).update(result = "Reply Received", + status = 1) + return reply + + else: + # Don't know what this is: ignore + return reply + # ------------------------------------------------------------------------- @staticmethod def parse_twitter(message): @@ -250,37 +297,6 @@ class S3Parser(object): # No reply here return None - # ------------------------------------------------------------------------- - @staticmethod - def _parse_keywords(message_body): - """ - Parse Keywords - - helper function for search_resource, etc - """ - - # Equivalent keywords in one list - primary_keywords = ["get", "give", "show"] - contact_keywords = ["email", "mobile", "facility", "clinical", - "security", "phone", "status", "hospital", - "person", "organisation"] - - pkeywords = primary_keywords + contact_keywords - keywords = message_body.split(" ") - pquery = [] - name = "" - for word in keywords: - match = None - for key in pkeywords: - if soundex(key) == soundex(word): - match = key - break - if match: - pquery.append(match) - else: - name = word - - return pquery, name - # ------------------------------------------------------------------------- def search_resource(self, message): """ @@ -310,7 +326,7 @@ class S3Parser(object): """ Search for People - can be called direct - - can be called from search_by_keyword + - can be called from search_resource """ message_body = message.body @@ -385,7 +401,7 @@ class S3Parser(object): """ Search for Hospitals - can be called direct - - can be called from search_by_keyword + - can be called from search_resource """ message_body = message.body @@ -458,7 +474,7 @@ class S3Parser(object): """ Search for Organisations - can be called direct - - can be called from search_by_keyword + - can be called from search_resource """ message_body = message.body @@ -600,6 +616,49 @@ class S3Parser(object): reply = "Incident Report Logged!" return reply + # ------------------------------------------------------------------------- + @staticmethod + def _parse_keywords(message_body): + """ + Parse Keywords + - helper function for search_resource, etc + """ + + # Equivalent keywords in one list + primary_keywords = ["get", "give", "show"] + contact_keywords = ["email", "mobile", "facility", "clinical", + "security", "phone", "status", "hospital", + "person", "organisation"] + + pkeywords = primary_keywords + contact_keywords + keywords = message_body.split(" ") + pquery = [] + name = "" + for word in keywords: + match = None + for key in pkeywords: + if soundex(key) == soundex(word): + match = key + break + if match: + pquery.append(match) + else: + name = word + + return pquery, name + + # ------------------------------------------------------------------------- + @staticmethod + def _parse_value(text, fieldname): + """ + Parse a value from a piece of text + """ + + parts = text.split(":%s:" % fieldname, 1) + parts = parts[1].split(":", 1) + result = parts[0] + return result + # ------------------------------------------------------------------------- @staticmethod def _respond_drequest(message, report_id, response, text): @@ -613,8 +672,8 @@ class S3Parser(object): rtable = current.s3db.irs_ireport_human_resource query = (rtable.ireport_id == report_id) & \ (rtable.human_resource_id == hr_id) - current.db(query).update(reply=text, - response=response) + current.db(query).update(reply = text, + response = response) reply = "Response Logged in the Report (Id: %d )" % report_id else: reply = None diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/supply_person_item_status.csv b/lxc-apps/sahana/install/sahana_data/Spotter/supply_person_item_status.csv new file mode 100644 index 0000000..3c6f741 --- /dev/null +++ b/lxc-apps/sahana/install/sahana_data/Spotter/supply_person_item_status.csv @@ -0,0 +1,2 @@ +Name,Comments +Received, \ No newline at end of file diff --git a/lxc-apps/sahana/install/sahana_data/Spotter/tasks.cfg b/lxc-apps/sahana/install/sahana_data/Spotter/tasks.cfg index c119a4c..af10945 100644 --- a/lxc-apps/sahana/install/sahana_data/Spotter/tasks.cfg +++ b/lxc-apps/sahana/install/sahana_data/Spotter/tasks.cfg @@ -14,13 +14,16 @@ # zzz_1st_run # s3import::S3BulkImporter # ============================================================================= -# Authentication +# Roles *,import_role,auth_roles.csv auth,user,masterUsers.csv,user.xsl # GIS +# Markers gis,marker,gis_marker.csv,marker.xsl +# Config gis,config,gis_config.csv,config.xsl gis,hierarchy,gis_hierarchy.csv,hierarchy.xsl +# Layers gis,layer_feature,gis_layer_feature.csv,layer_feature.xsl gis,layer_config,gis_layer_openstreetmap.csv,layer_openstreetmap.xsl gis,layer_config,gis_layer_openweathermap.csv,layer_openweathermap.xsl @@ -31,16 +34,15 @@ gis,layer_config,gis_layer_tms.csv,layer_tms.xsl gis,layer_geojson,gis_layer_geojson.csv,layer_geojson.xsl gis,layer_georss,gis_layer_georss.csv,layer_georss.xsl gis,layer_config,gis_layer_coordinate.csv,layer_coordinate.xsl -# CMS +# ----------------------------------------------------------------------------- cms,post,cms_post.csv,post.xsl -# Organisations +# ----------------------------------------------------------------------------- org,sector,org_sector.csv,sector.xsl org,organisation_type,organisation_type.csv,organisation_type.xsl org,office_type,office_type.csv,office_type.xsl -# Supplies supply,catalog_item,DefaultItems.csv,catalog_item.xsl supply,catalog_item,StandardItems.csv,catalog_item.xsl -# Human Resource Management +supply,person_item_status,supply_person_item_status.csv,person_item_status.xsl hrm,skill,DefaultSkillList.csv,skill.xsl hrm,skill,DrivingSkillList.csv,skill.xsl hrm,skill,DrivingSkillList_EU.csv,skill.xsl @@ -48,20 +50,15 @@ hrm,skill,LanguageSkillList.csv,skill.xsl hrm,competency_rating,DefaultSkillCompetency.csv,competency_rating.xsl hrm,competency_rating,LanguageCompetency.csv,competency_rating.xsl hrm,certificate,certificate.csv,certificate.xsl -# Events -event,event_type,event_type.csv,event_type.xsl -event,incident_type,incident_type.csv,incident_type.xsl -# Projects project,status,project_status.csv,status.xsl project,activity_type,project_activity_type.csv,activity_type.xsl project,hazard,project_hazard.csv,hazard.xsl project,theme,project_theme.csv,theme.xsl project,beneficiary_type,project_beneficiary_type.csv,beneficiary_type.xsl -# Shelters -cr,shelter_type,shelter_type.csv,shelter_type.xsl -# Disaster Victim Registry -dvr,case_status,dvr_case_status.csv,case_status.xsl -# Members +# Spotter +cr,shelter_type,cr_shelter_type.csv,shelter_type.xsl +event,event_type,event_type.csv,event_type.xsl +event,incident_type,incident_type.csv,incident_type.xsl member,membership_type,membership_type.csv,membership_type.xsl -# Jobs work,job_type,work_job_type.csv,job_type.xsl +# =============================================================================