Validate user inputs in settings daemon

Currently, the properties defined under /org/openbmc/settings/host0 can take any
value and it is upto the consumer daemons to act only if the valid data was
given.

This patch will provide a validation logic for all the properties and will raise
an exception in the case of invalid inputs. Only on a valid input, will the data
gets written and saved.

Validation methods and types per property are provided in the configuration file
and used by the manager whenever a particular property is changed.

Change-Id: I0731ce6e00ab3cb4e11deb98c03fda8d5adad913
Signed-off-by: Vishwanatha Subbanna <vishwa@linux.vnet.ibm.com>
diff --git a/settings.yaml b/settings.yaml
index 9e08e29..ca26840 100644
--- a/settings.yaml
+++ b/settings.yaml
@@ -1,6 +1,6 @@
 ---
 # Settings Config File
-host:
+org.openbmc.settings.Host:
     powercap:
         name: power_cap
         type: i
@@ -8,39 +8,66 @@
         min: 0
         max: 1000
         unit: watts
+        validation: range
     bootflags:
         name: boot_flags
         type: s
         default: "default"
+        validation: list
+        allowed: ["Network", "Disk", "Safe", "CDROM", "Setup", "default"]
     sysstate:
         name: system_state
         type: s
         default: ""
+        validation: None
     powerpolicy:
         name: power_policy
         type: s
         default: "RESTORE_LAST_STATE"
+        validation: list
+        allowed: ["ALWAYS_POWER_ON", "RESTORE_LAST_STATE", "LEAVE_OFF"]
     restrictedmode:
         name: restricted_mode
         type: b
         default: false
+        min: 0
+        max: 1
+        validation: range
     bootpolicy:
         name: boot_policy
         type: s
         default: "ONETIME"
+        validation: list
+        allowed: ["ONETIME", "PERMANENT"]
     networkconfig:
         name: network_config
         type: s
         default: "ipaddress=,prefix=,gateway=,mac=,addr_type="
+        validation: custom
+        method: validate_net_config
     TimeMode:
         name: time_mode
         type: s
         default: "NTP"
+        validation: list
+        allowed: ["NTP", "MANUAL"]
     TimeOwner:
         name: time_owner
         type: s
         default: "BMC"
+        validation: list
+        allowed: ["BMC", "HOST", "SPLIT", "BOTH"]
     UseDhcpNtp:
         name: use_dhcp_ntp
         type: s
         default: "yes"
+        validation: list
+        allowed: ["yes", "no"]
+
+# Example of using regex
+#    macaddress:
+#        name: mac_address
+#        type: s
+#        default: "aa:bb:cc:dd:ee:ff"
+#        validation: regex
+#        regex: '([a-fA-F0-9]{2}[:|\-]?){6}'
diff --git a/settings_file.py b/settings_file.py
index ced0d5e..f0c6d13 100644
--- a/settings_file.py
+++ b/settings_file.py
@@ -1,30 +1,65 @@
 #!/usr/bin/python -u
 SETTINGS=\
-{'host': {'TimeMode': {'default': 'NTP', 'name': 'time_mode', 'type': 's'},
-          'TimeOwner': {'default': 'BMC', 'name': 'time_owner', 'type': 's'},
-          'UseDhcpNtp': {'default': 'yes',
-                         'name': 'use_dhcp_ntp',
-                         'type': 's'},
-          'bootflags': {'default': 'default',
-                        'name': 'boot_flags',
-                        'type': 's'},
-          'bootpolicy': {'default': 'ONETIME',
-                         'name': 'boot_policy',
-                         'type': 's'},
-          'networkconfig': {'default':
-              'ipaddress=,prefix=,gateway=,mac=,addr_type=',
-                            'name': 'network_config',
-                            'type': 's'},
-          'powercap': {'default': 0,
-                       'max': 1000,
-                       'min': 0,
-                       'name': 'power_cap',
-                       'type': 'i',
-                       'unit': 'watts'},
-          'powerpolicy': {'default': 'RESTORE_LAST_STATE',
-                          'name': 'power_policy',
-                          'type': 's'},
-          'restrictedmode': {'default': False,
-                             'name': 'restricted_mode',
-                             'type': 'b'},
-          'sysstate': {'default': '', 'name': 'system_state', 'type': 's'}}}
+{'org.openbmc.settings.Host': {'TimeMode': {'allowed': ['NTP', 'MANUAL'],
+                                            'default': 'NTP',
+                                            'name': 'time_mode',
+                                            'type': 's',
+                                            'validation': 'list'},
+                               'TimeOwner': {'allowed': ['BMC',
+                                                         'HOST',
+                                                         'SPLIT',
+                                                         'BOTH'],
+                                             'default': 'BMC',
+                                             'name': 'time_owner',
+                                             'type': 's',
+                                             'validation': 'list'},
+                               'UseDhcpNtp': {'allowed': ['yes', 'no'],
+                                              'default': 'yes',
+                                              'name': 'use_dhcp_ntp',
+                                              'type': 's',
+                                              'validation': 'list'},
+                               'bootflags': {'allowed': ['Network',
+                                                         'Disk',
+                                                         'Safe',
+                                                         'CDROM',
+                                                         'Setup',
+                                                         'default'],
+                                             'default': 'default',
+                                             'name': 'boot_flags',
+                                             'type': 's',
+                                             'validation': 'list'},
+                               'bootpolicy': {'allowed': ['ONETIME',
+                                                          'PERMANENT'],
+                                              'default': 'ONETIME',
+                                              'name': 'boot_policy',
+                                              'type': 's',
+                                              'validation': 'list'},
+                               'networkconfig': {'default': 'ipaddress=,prefix=,gateway=,mac=,addr_type=',
+                                                 'method': 'validate_net_config',
+                                                 'name': 'network_config',
+                                                 'type': 's',
+                                                 'validation': 'custom'},
+                               'powercap': {'default': 0,
+                                            'max': 1000,
+                                            'min': 0,
+                                            'name': 'power_cap',
+                                            'type': 'i',
+                                            'unit': 'watts',
+                                            'validation': 'range'},
+                               'powerpolicy': {'allowed': ['ALWAYS_POWER_ON',
+                                                           'RESTORE_LAST_STATE',
+                                                           'LEAVE_OFF'],
+                                               'default': 'RESTORE_LAST_STATE',
+                                               'name': 'power_policy',
+                                               'type': 's',
+                                               'validation': 'list'},
+                               'restrictedmode': {'default': False,
+                                                  'max': 1,
+                                                  'min': 0,
+                                                  'name': 'restricted_mode',
+                                                  'type': 'b',
+                                                  'validation': 'range'},
+                               'sysstate': {'default': '',
+                                            'name': 'system_state',
+                                            'type': 's',
+                                            'validation': 'None'}}}
diff --git a/settings_manager.py b/settings_manager.py
index 864ee9d..997c56f 100644
--- a/settings_manager.py
+++ b/settings_manager.py
@@ -8,10 +8,12 @@
 import os.path as path
 import sys
 from obmc.dbuslib.bindings import DbusProperties, get_dbus
+from IPy import IP
 
 settings_file_path = os.path.join(sys.prefix, 'share/obmc-phosphor-settings')
 sys.path.insert(1, settings_file_path)
 import settings_file as s
+import re
 
 DBUS_NAME = 'org.openbmc.settings.Host'
 OBJ_NAME = '/org/openbmc/settings/host0'
@@ -20,10 +22,14 @@
 
 class HostSettingsObject(DbusProperties):
     def __init__(self, bus, name, settings, path):
-        DbusProperties.__init__(self)
-        dbus.service.Object.__init__(self, bus, name)
-
+        super(HostSettingsObject, self).__init__(conn=bus, object_path=name,
+                                                validator=self.input_validator)
+        self.bus = bus
         self.path = path
+        # Needed to ignore the validation on default networkconfig values as
+        # opposed to user giving the same.
+        self.adminmode = True
+
         if not os.path.exists(path):
             os.mkdir(path)
 
@@ -35,11 +41,13 @@
             path="/org/openbmc/settings/host0")
 
         # Create the dbus properties
-        for i in settings['host'].iterkeys():
-            shk = settings['host'][i]
+        for i in settings[DBUS_NAME].iterkeys():
+            shk = settings[DBUS_NAME][i]
             self.set_settings_property(shk['name'],
                                        shk['type'],
                                        shk['default'])
+        # Done with consuming factory settings.
+        self.adminmode = False
 
     def get_bmc_value(self, name):
         try:
@@ -58,11 +66,11 @@
         if bmcv:
             value = bmcv
         if type == "i":
-            self.Set(DBUS_NAME, name, value)
+            self.Set(DBUS_NAME, name, int(value))
         elif type == "s":
             self.Set(DBUS_NAME, name, str(value))
         elif type == "b":
-            self.Set(DBUS_NAME, name, value)
+            self.Set(DBUS_NAME, name, bool(value))
 
     # Save the settings to the BMC. This will write the settings value in
     # individual files named by the property name to the BMC.
@@ -85,6 +93,85 @@
     def SettingsUpdated(self, sname):
         pass
 
+    def validate_regex(self, regex, value):
+        if not re.compile(regex).search(value):
+            raise ValueError, "Invalid input. Data does not satisfy regex"
+
+    def validate_range(self, min, max, value):
+        if value not in range(min, max):
+            raise ValueError, "Invalid input. Data not in allowed range"
+
+    def validate_list_ignore_case(self, lst, value):
+        if value.lower() not in map(lambda val: val.lower(), lst):
+            raise ValueError, "Invalid input. Data not in allowed values"
+
+    # validate host network configuration
+    # need  "ipaddress=,prefix=,gateway=,mac=,addr_type="
+    # Must be able to handle any order
+    def validate_net_config(self, value):
+        if self.adminmode:
+            return
+
+        # Need all of these to be given by the user.
+        user_config = []
+        all_config = ['ipaddress', 'prefix', 'gateway', 'mac', 'addr_type']
+
+        # This has a hard data format mentioned above so no blanks allowed.
+        if value.count(" ") or value.count("=") != 5:
+            raise ValueError, "Invalid Network Data. No white spaces allowed"
+
+        config = value.split(',')
+        for key_value in config:
+            key , value = key_value.split('=')
+            if not key or not value:
+                raise ValueError, "Invalid key or Data"
+
+            # Add the current key seen so we can compare at the end to see
+            # if all values have been given
+            user_config.append(key.lower())
+
+            if key.lower() == 'ipaddress' or key.lower() == 'gateway':
+	        IP(value)
+
+            elif key.lower() == 'mac':
+                regex = '([a-fA-F0-9]{2}[:|\-]?){6}'
+                self.validate_regex(regex, value)
+
+            elif key.lower() == 'prefix':
+                self.validate_range(0, 33, int(value))
+
+            elif key.lower() == 'addr_type':
+                allowed = ["STATIC", "DYNAMIC"]
+                self.validate_list_ignore_case(allowed, value)
+
+        # Did user pass everything ??
+        if set(all_config) - set(user_config):
+            raise ValueError, "Invalid Network Data. All information is mandatory"
+
+    # Validate to see if the changes are in order
+    def input_validator(self, iface, proprty, value):
+        settings = s.SETTINGS
+        shk = {}
+        for key in settings[iface].iterkeys():
+            if proprty == settings[iface][key]['name']:
+                shk = settings[iface][key]
+                break
+
+        # User entered key is not present
+        if not shk: raise KeyError, "Invalid Property"
+
+        if shk['validation'] == 'list':
+            self.validate_list_ignore_case(shk['allowed'], value)
+
+        elif shk['validation'] == 'range':
+            self.validate_range(shk['min'], shk['max']+1, value)
+
+        elif shk['validation'] == 'regex':
+            self.validate_regex(shk['regex'], value)
+
+        elif shk['validation'] == 'custom':
+	    getattr(self, shk['method'])(value)
+
 if __name__ == '__main__':
     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)