SEBA-513 Validation of xproto

Change-Id: I300e86c3b7b6839aa12d726d6bdf9ab59adece94
diff --git a/VERSION b/VERSION
index 23a63f5..a6333e4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.2.8
+2.2.9
diff --git a/containers/chameleon/Dockerfile.chameleon b/containers/chameleon/Dockerfile.chameleon
index ce1dd45..51fe650 100644
--- a/containers/chameleon/Dockerfile.chameleon
+++ b/containers/chameleon/Dockerfile.chameleon
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/chameleon
-FROM xosproject/xos-base:2.2.8
+FROM xosproject/xos-base:2.2.9
 
 # xos-base already has protoc and dependencies installed
 
diff --git a/containers/xos/Dockerfile.client b/containers/xos/Dockerfile.client
index 775524a..5f0e13f 100644
--- a/containers/xos/Dockerfile.client
+++ b/containers/xos/Dockerfile.client
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-client
-FROM xosproject/xos-libraries:2.2.8
+FROM xosproject/xos-libraries:2.2.9
 
 # Install XOS client
 COPY lib/xos-api /tmp/xos-api
diff --git a/containers/xos/Dockerfile.libraries b/containers/xos/Dockerfile.libraries
index ad02937..57aa334 100644
--- a/containers/xos/Dockerfile.libraries
+++ b/containers/xos/Dockerfile.libraries
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-libraries
-FROM xosproject/xos-base:2.2.8
+FROM xosproject/xos-base:2.2.9
 
 # Add libraries
 COPY lib /opt/xos/lib
diff --git a/containers/xos/Dockerfile.synchronizer-base b/containers/xos/Dockerfile.synchronizer-base
index 1fe2e05..3e5793a 100644
--- a/containers/xos/Dockerfile.synchronizer-base
+++ b/containers/xos/Dockerfile.synchronizer-base
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-synchronizer-base
-FROM xosproject/xos-client:2.2.8
+FROM xosproject/xos-client:2.2.9
 
 COPY xos/synchronizers/new_base /opt/xos/synchronizers/new_base
 COPY xos/xos/logger.py /opt/xos/xos/logger.py
diff --git a/containers/xos/Dockerfile.xos-core b/containers/xos/Dockerfile.xos-core
index 916d147..1c3d962 100644
--- a/containers/xos/Dockerfile.xos-core
+++ b/containers/xos/Dockerfile.xos-core
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-core
-FROM xosproject/xos-libraries:2.2.8
+FROM xosproject/xos-libraries:2.2.9
 
 # Install XOS
 ADD xos /opt/xos
diff --git a/lib/xos-genx/xosgenx/generator.py b/lib/xos-genx/xosgenx/generator.py
index 9a6a249..ec21d34 100644
--- a/lib/xos-genx/xosgenx/generator.py
+++ b/lib/xos-genx/xosgenx/generator.py
@@ -19,10 +19,12 @@
 import plyxproto.parser as plyxproto
 import yaml
 from colorama import Fore
+import sys
 
 from . import jinja2_extensions
 from .proto2xproto import Proto2XProto
 from .xos2jinja import XOS2Jinja
+from .validator import XProtoValidator
 
 loader = jinja2.PackageLoader(__name__, "templates")
 env = jinja2.Environment(loader=loader)
@@ -49,6 +51,7 @@
         []
     )  # If neither include_models nor include_apps is specified, then all models will
     default_include_apps = []  # be included.
+    default_strict_validation = False
 
     def __init__(self, **kwargs):
         # set defaults
@@ -64,6 +67,7 @@
         self.default_checkers = XOSProcessorArgs.default_target
         self.include_models = XOSProcessorArgs.default_include_models
         self.include_apps = XOSProcessorArgs.default_include_apps
+        self.strict_validation = XOSProcessorArgs.default_strict_validation
 
         # override defaults with kwargs
         for (k, v) in kwargs.items():
@@ -73,11 +77,18 @@
 class XOSProcessor:
     @staticmethod
     def _read_input_from_files(files):
+        """ Read the files and return the combined text read.
+
+            Also returns a list of (line_number, filename) tuples that tell which
+            starting line corresponds to each file.
+        """
+        line_map = []
         input = ""
         for fname in files:
             with open(fname) as infile:
+                line_map.append( (len(input.split("\n")), fname) )
                 input += infile.read()
-        return input
+        return (input, line_map)
 
     @staticmethod
     def _attach_parser(ast, args):
@@ -249,9 +260,10 @@
             raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
 
         if hasattr(args, "files"):
-            inputs = XOSProcessor._read_input_from_files(args.files)
+            (inputs, line_map) = XOSProcessor._read_input_from_files(args.files)
         elif hasattr(args, "inputs"):
             inputs = args.inputs
+            line_map = []
         else:
             raise Exception("[XosGenX] No inputs provided!")
 
@@ -320,6 +332,14 @@
             for message in v.messages:
                 message["is_included"] = True
 
+        validator = XProtoValidator(v.models, line_map)
+        validator.validate()
+        if validator.errors:
+            if args.strict_validation or (args.verbosity>=0):
+                validator.print_errors()
+            if args.strict_validation:
+                sys.exit(-1)
+
         if args.output is not None and args.write_to_file == "model":
             rendered = {}
             for i, model in enumerate(v.models):
diff --git a/lib/xos-genx/xosgenx/jinja2_extensions/django.py b/lib/xos-genx/xosgenx/jinja2_extensions/django.py
index 0c34d3e..6fac823 100644
--- a/lib/xos-genx/xosgenx/jinja2_extensions/django.py
+++ b/lib/xos-genx/xosgenx/jinja2_extensions/django.py
@@ -124,11 +124,7 @@
 
     if modifier == "required":
 
-        if field_type == "string":
-            if "blank" not in out:  # if blank is already set, honor that value
-                mod_out["blank"] = 'True'  # by default, required strings can be blank
-        else:
-            mod_out["blank"] = 'False'  # but other required fields can't be blank
+        mod_out["blank"] = 'False'
 
         if link_type != "manytomany":
             mod_out["null"] = 'False'
diff --git a/lib/xos-genx/xosgenx/validator.py b/lib/xos-genx/xosgenx/validator.py
new file mode 100644
index 0000000..384689b
--- /dev/null
+++ b/lib/xos-genx/xosgenx/validator.py
@@ -0,0 +1,246 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+  This module is used to validate xproto models and fields. The basic guiding principle is everything that isn't
+  specifically allowed here should be denied by default.
+
+  Note: While xproto must maintain some compatibility with django give the implementation choice of using django
+  in the core, it's the case that the allowable set of xproto options may be a subset of what is allowed under
+  django. For example, there may be django features that do not need exposure in xproto and/or are incompatible
+  with other design aspects of XOS such as the XOS gRPC API  implementation.
+"""
+
+
+from __future__ import print_function
+import sys
+import os
+
+
+# Options that are always allowed
+COMMON_OPTIONS = ["help_text", "gui_hidden", "tosca_key", "tosca_key_one_of", "feedback_state", "unique", "unique_with"]
+
+# Options that must be either "True" or "False"
+BOOLEAN_OPTIONS = ["blank", "db_index", "feedback_state", "gui_hidden", "null", "tosca_key", "unique", "varchar"]
+
+class XProtoValidator(object):
+    def __init__(self, models, line_map):
+        """
+            models: a list of model definitions. Each model is a dictionary.
+            line_map: a list of tuples (start_line_no, filename) that tells which file goes with which line number.
+        """
+        self.models = models
+        self.line_map = line_map
+        self.errors = []
+
+    def error(self, model, field, message):
+        if field and field.get("_linespan"):
+            error_first_line_number = field["_linespan"][0]
+            error_last_line_number = field["_linespan"][1]
+        else:
+            error_first_line_number = model["_linespan"][0]
+            error_last_line_number = model["_linespan"][1]
+
+        error_filename = "unknown"
+        error_line_offset = 0
+        for (start_line, fn) in self.line_map:
+            if start_line>error_first_line_number:
+                break
+            error_filename = fn
+            error_line_offset = start_line
+
+        self.errors.append({"model": model,
+                            "field": field,
+                            "message": message,
+                            "filename": error_filename,
+                            "first_line_number": error_first_line_number - error_line_offset,
+                            "last_line_number": error_last_line_number - error_line_offset,
+                            "absolute_line_number": error_first_line_number})
+
+    def print_errors(self):
+        # Sort by line number
+        for error in sorted(self.errors, key=lambda error:error["absolute_line_number"]):
+            model = error["model"]
+            field = error["field"]
+            message = error["message"]
+            first_line_number = error["first_line_number"]
+            last_line_number = error["last_line_number"]
+
+            if first_line_number != last_line_number:
+                linestr = "%d-%d" % (first_line_number, last_line_number)
+            else:
+                linestr = "%d" % first_line_number
+
+            print("[ERROR] %s:%s %s.%s (Type %s): %s" % (os.path.basename(error["filename"]),
+                                                           linestr,
+                                                           model.get("name"),
+                                                           field.get("name"),
+                                                           field.get("type"),
+                                                           message), file=sys.stderr)
+
+    def is_option_true(self, field, name):
+        options = field.get("options")
+        if not options:
+            return False
+        option = options.get(name)
+        return option == "True"
+
+    def allow_options(self, model, field, options):
+        """ Only allow the options specified in `options`. If some option is present that isn't in allowed, then
+            register an error.
+
+            `options` is a list of options which can either be simple names, or `name=value`.
+        """
+        options = COMMON_OPTIONS + options
+
+        for (k, v) in field.get("options", {}).items():
+            allowed = False
+            for option in options:
+                if "=" in option:
+                    (optname, optval) = option.split("=")
+                    if optname==k and optval==v:
+                        allowed = True
+                else:
+                    if option==k:
+                        allowed = True
+
+            if not allowed:
+                self.error(model, field, "Option %s=%s is not allowed" % (k,v))
+
+            if k in BOOLEAN_OPTIONS and (v not in ["True", "False"]):
+                self.error(model, field, "Option `%s` must be either True or False, but is '%s'" % (k, v))
+
+    def require_options(self, model, field, options):
+        """ Require an option to be present.
+        """
+        for optname in options:
+            if not field.get(optname):
+                self.error(model, field, "Required option '%s' is not present" % optname)
+
+    def check_modifier_consistent(self, model, field):
+        """ Validates that "modifier" is consistent with options.
+        
+            Required/optional imply some settings for blank= and null=. These settings are dependent on the type
+            of field. See also jinja2_extensions/django.py which has to implement some of the same logic.
+        """
+        field_type = field["type"]
+        options = field.get("options", {})
+        modifier = options.get('modifier')
+        link_type = field.get("link_type")
+        mod_out = {}
+
+        if modifier == "required":
+            mod_out["blank"] = 'False'
+
+            if link_type != "manytomany":
+                mod_out["null"] = 'False'
+
+        elif modifier == "optional":
+            mod_out["blank"] = 'True'
+
+            # set defaults on link types
+            if link_type != "manytomany" and field_type != "bool":
+                mod_out["null"] = 'True'
+
+        else:
+            self.error(model, field, "Unknown modifier type '%s'" % modifier)
+
+        # print an error if there's a field conflict
+        for kmo in mod_out.keys():
+            if (kmo in options) and (options[kmo] != mod_out[kmo]):
+                self.error(model, field, "Option `%s`=`%s` is inconsistent with modifier `%s`" % (kmo, options[kmo], modifier))
+
+    def validate_field_date(self, model, field):
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field, ["auto_now_add", "blank", "db_index", "default", "max_length", "modifier", "null", "content_type"])
+
+    def validate_field_string(self, model, field):
+        # A string with a `content_type="date"` is actually a date
+        # TODO: Investigate why there are double-quotes around "date"
+        content_type = field.get("options", {}).get("content_type")
+        if content_type in ["\"date\""]:
+            self.validate_field_date(model, field)
+            return
+
+        # TODO: Investigate why there are double-quotes around the content types
+        if content_type and content_type not in ["\"stripped\"", "\"ip\"", "\"url\""]:
+            self.error(model, field, "Content type %s is not allowed" % content_type)
+
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field,
+                           ["blank", "choices", "content_type", "db_index", "default", "max_length", "modifier", "null",
+                            "varchar"])
+
+    def validate_field_bool(self, model, field):
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field, ["db_index", "default=True", "default=False", "modifier", "null=False"])
+        self.require_options(model, field, ["default"])
+
+    def validate_field_float(self, model, field):
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field, ["blank", "db_index", "default", "modifier", "null"])
+
+    def validate_field_link_onetomany(self, model, field):
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field,
+                           ["blank", "db_index", "default", "model", "link_type=manytoone",
+                            "modifier", "null", "port", "type=link"])
+
+    def validate_field_link_manytomany(self, model, field):
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field,
+                           ["blank", "db_index", "default", "model", "link_type=manytomany",
+                            "modifier", "null", "port", "type=link"])
+
+    def validate_field_link(self, model, field):
+        link_type = field.get("options",{}).get("link_type")
+        if link_type == "manytoone":
+            self.validate_field_link_onetomany(model, field)
+        elif link_type == "manytomany":
+            self.validate_field_link_manytomany(model, field)
+        else:
+            self.error("Unknown link_type %s" % link_type)
+
+    def validate_field_integer(self, model, field):
+        # An integer with an option "type=link" is actually a link
+        if field.get("options", {}).get("type") == "link":
+            self.validate_field_link(model, field)
+            return
+
+        self.check_modifier_consistent(model, field)
+        self.allow_options(model, field, ["blank", "db_index", "default", "max_value", "min_value", "modifier", "null"])
+
+        if self.is_option_true(field, "blank") and not self.is_option_true(field, "null"):
+            self.error(model, field, "If blank is true then null must also be true")
+
+    def validate_field(self, model, field):
+        if field["type"] == "string":
+            self.validate_field_string(model, field)
+        elif field["type"] in ["int32", "uint32"]:
+            self.validate_field_integer(model, field)
+        elif field["type"] == "float":
+            self.validate_field_float(model, field)
+        elif field["type"] == "bool":
+            self.validate_field_bool(model, field)
+        else:
+            self.error(model, field, "Unknown field type %s" % field["type"])
+
+    def validate_model(self, model):
+        for field in model["fields"]:
+            self.validate_field(model, field)
+
+    def validate(self):
+        """ Validate all models. This is the main entrypoint for validating xproto. """
+        for (name, model) in self.models.items():
+            self.validate_model(model)
\ No newline at end of file
diff --git a/lib/xos-genx/xosgenx/xos2jinja.py b/lib/xos-genx/xosgenx/xos2jinja.py
index e4ff7db..c61bf55 100644
--- a/lib/xos-genx/xosgenx/xos2jinja.py
+++ b/lib/xos-genx/xosgenx/xos2jinja.py
@@ -267,6 +267,7 @@
         except BaseException:
             pass
         s["_type"] = "field"
+        s["_linespan"] = obj.linespan
 
         self.stack.push(s)
         return True
@@ -327,6 +328,7 @@
             "package": self.package,
             "fqn": model_name,
             "rlinks": [],
+            "_linespan": obj.linespan,       # first and last line number
         }
         try:
             model_def["policy"] = obj.policy.pval
diff --git a/lib/xos-genx/xosgenx/xosgen.py b/lib/xos-genx/xosgenx/xosgen.py
index 2da8962..608cae7 100755
--- a/lib/xos-genx/xosgenx/xosgen.py
+++ b/lib/xos-genx/xosgenx/xosgen.py
@@ -125,6 +125,14 @@
     help="xproto files to compile",
 )
 
+parse.add_argument(
+    "--strict-validation",
+    dest="strict_validation",
+    action="store_true",
+    default=XOSProcessorArgs.default_strict_validation,
+    help="Exit if validation fails",
+)
+
 CHECK = 1
 GEN = 2
 
diff --git a/xos/core/migrations/0007_auto_20190307_1227.py b/xos/core/migrations/0007_auto_20190307_1227.py
new file mode 100644
index 0000000..b029d75
--- /dev/null
+++ b/xos/core/migrations/0007_auto_20190307_1227.py
@@ -0,0 +1,546 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.11 on 2019-03-07 17:27
+from __future__ import unicode_literals
+
+import core.models.xosbase_header
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0006_auto_20190304_2346'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='cidr',
+            field=models.CharField(help_text=b'Subnet for this AddressPool', max_length=32),
+        ),
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='gateway_ip',
+            field=models.CharField(help_text=b'Gateway IP address for this AddressPool', max_length=32),
+        ),
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='gateway_mac',
+            field=models.CharField(help_text=b'Gateway MAC address for this AddressPool', max_length=32),
+        ),
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='addresspool_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='flavor_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='flavor_decl',
+            name='flavor',
+            field=core.models.xosbase_header.StrippedCharField(help_text=b'flavor string used to configure deployments', max_length=32),
+        ),
+        migrations.AlterField(
+            model_name='flavor_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='flavor_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='image_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='image_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='image_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='interfacetype_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='interfacetype_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='interfacetype_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='end_ip',
+            field=models.CharField(blank=True, max_length=32, null=True),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='start_ip',
+            field=models.CharField(blank=True, max_length=32, null=True),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='subnet',
+            field=models.CharField(blank=True, max_length=32, null=True),
+        ),
+        migrations.AlterField(
+            model_name='network_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='networkparameter_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkparameter_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkparameter_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='networkparametertype_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkparametertype_decl',
+            name='description',
+            field=models.CharField(blank=True, max_length=1024, null=True),
+        ),
+        migrations.AlterField(
+            model_name='networkparametertype_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkparametertype_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='networkslice_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkslice_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networkslice_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='networktemplate_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networktemplate_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='networktemplate_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='node_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='node_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='node_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='nodelabel_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='nodelabel_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='nodelabel_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='port_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='port_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='port_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='principal_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='principal_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='principal_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='privilege_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='privilege_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='privilege_decl',
+            name='permission',
+            field=models.CharField(default=b'all', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='privilege_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='role_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='role_decl',
+            name='description',
+            field=core.models.xosbase_header.StrippedCharField(blank=True, max_length=120, null=True),
+        ),
+        migrations.AlterField(
+            model_name='role_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='role_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='service_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='service_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='service_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceattribute_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceattribute_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceattribute_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='servicedependency_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='servicedependency_decl',
+            name='connect_method',
+            field=models.CharField(choices=[(b'none', b'None'), (b'private', b'Private'), (b'public', b'Public')], default=b'none', help_text=b'method to connect the two services', max_length=30),
+        ),
+        migrations.AlterField(
+            model_name='servicedependency_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='servicedependency_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='servicegraphconstraint_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='servicegraphconstraint_decl',
+            name='constraints',
+            field=core.models.xosbase_header.StrippedCharField(help_text=b'A composite array defining positions, eg [volt, vsg, [address_manager, vrouter]]', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='servicegraphconstraint_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='servicegraphconstraint_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstance_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstance_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstance_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstanceattribute_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstanceattribute_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstanceattribute_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstancelink_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstancelink_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinstancelink_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceinterface_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinterface_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceinterface_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='serviceport_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceport_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='serviceport_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='site_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='site_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='site_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='slice_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='slice_decl',
+            name='description',
+            field=models.CharField(blank=True, help_text=b'High level description of the slice and expected activities', max_length=1024, null=True),
+        ),
+        migrations.AlterField(
+            model_name='slice_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='slice_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='tag_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='tag_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='tag_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='trustdomain_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='trustdomain_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='trustdomain_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='xoscore_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='xoscore_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='xoscore_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+        migrations.AlterField(
+            model_name='xosguiextension_decl',
+            name='backend_status',
+            field=models.CharField(default=b'Provisioning in progress', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='xosguiextension_decl',
+            name='leaf_model_name',
+            field=models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024),
+        ),
+        migrations.AlterField(
+            model_name='xosguiextension_decl',
+            name='updated',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer'),
+        ),
+    ]
diff --git a/xos/core/models/core.xproto b/xos/core/models/core.xproto
index 3c0d63b..12d00af 100644
--- a/xos/core/models/core.xproto
+++ b/xos/core/models/core.xproto
@@ -162,7 +162,7 @@
 
 message Flavor (XOSBase) {
      required string name = 1 [max_length = 32, content_type = "stripped", blank = False, help_text = "name of this flavor, as displayed to users", null = False, db_index = False, unique = True];
-     optional string description = 2 [db_index = False, max_length = 1024, null = True, content_type = "stripped", blank = True];
+     optional string description = 2 [db_index = False, max_length = 1024, null = True, content_type = "stripped"];
      required string flavor = 3 [max_length = 32, content_type = "stripped", blank = True, help_text = "flavor string used to configure deployments", null = False, db_index = False];
 }
 
@@ -181,11 +181,11 @@
 message Network::network_policy (XOSBase) {
      required string name = 1 [db_index = False, max_length = 32, null = False, blank = False, unique = True];
      required manytoone template->NetworkTemplate:network = 2:1001 [db_index = True, null = False, blank = False];
-     required string subnet = 3 [db_index = False, max_length = 32, null = False, blank = True];
-     required string start_ip = 4 [db_index = False, max_length = 32, null = False, blank = True];
-     required string end_ip = 5 [db_index = False, max_length = 32, null = False, blank = True];
-     optional string ports = 6 [db_index = False, max_length = 1024, null = True, blank = True];
-     optional string labels = 7 [db_index = False, max_length = 1024, null = True, blank = True];
+     optional string subnet = 3 [db_index = False, max_length = 32];
+     optional string start_ip = 4 [db_index = False, max_length = 32];
+     optional string end_ip = 5 [db_index = False, max_length = 32];
+     optional string ports = 6 [db_index = False, max_length = 1024];
+     optional string labels = 7 [db_index = False, max_length = 1024];
      required manytoone owner->Slice:ownedNetworks = 8:1004 [help_text = "Slice that owns control of this Network", null = False, db_index = True, blank = False];
      required bool permit_all_slices = 10 [default = False, null = False, db_index = False, blank = True];
      required bool autoconnect = 17 [help_text = "This network can be autoconnected to the slice that owns it", default = True, null = False, db_index = False, blank = True];
@@ -203,7 +203,7 @@
 
 message NetworkParameterType (XOSBase) {
      required string name = 1 [help_text = "The name of this parameter", max_length = 128, null = False, db_index = True, blank = False, unique = True];
-     required string description = 2 [db_index = False, max_length = 1024, null = False, blank = True];
+     optional string description = 2 [db_index = False, max_length = 1024, null = False];
 }
 
 policy network_slice_validator < (obj.slice in obj.network.permitted_slices.all()) | (obj.slice = obj.network.owner) | obj.network.permit_all_slices >
@@ -217,7 +217,7 @@
 
 message NetworkTemplate (XOSBase) {
      required string name = 1 [db_index = False, max_length = 32, null = False, blank = False, unique = True];
-     optional string description = 2 [db_index = False, max_length = 1024, null = True, blank = True];
+     optional string description = 2 [db_index = False, max_length = 1024];
      required string visibility = 4 [default = "private", choices = "(('public', 'public'), ('private', 'private'))", max_length = 30, blank = False, null = False, db_index = False];
      required string translation = 5 [default = "none", choices = "(('none', 'none'), ('NAT', 'NAT'))", max_length = 30, blank = False, null = False, db_index = False];
      optional string access = 6 [choices = "((None, 'None'), ('indirect', 'Indirect'), ('direct', 'Direct'))", max_length = 30, blank = True, help_text = "Advertise this network as a means for other slices to contact this slice", null = True, db_index = False];
@@ -260,7 +260,7 @@
 message Role (XOSBase) {
      required string role_type = 1 [db_index = False, max_length = 80, null = False, content_type = "stripped", blank = False];
      optional string role = 2 [db_index = False, max_length = 80, null = True, content_type = "stripped", blank = True];
-     required string description = 3 [db_index = False, max_length = 120, null = False, content_type = "stripped", blank = False];
+     optional string description = 3 [db_index = False, max_length = 120, content_type = "stripped"];
 }
 
 policy service_policy <ctx.user.is_admin | exists Privilege: Privilege.accessor_id = ctx.user.id & Privilege.accessor_type = "User" & Privilege.object_type = "Service" & Privilege.object_id = obj.id >
@@ -270,10 +270,8 @@
 
      optional string description = 1 [
          help_text = "Description of Service",
-         blank = True,
          db_index = False,
          max_length = 254,
-         null = True,
          varchar = True];
      required bool enabled = 2 [
          help_text = "Whether or not service is Enabled",
@@ -450,7 +448,7 @@
 
      required string name = 1 [max_length = 80, content_type = "stripped", blank = False, help_text = "The Name of the Slice", null = False, db_index = False, unique = True];
      required bool enabled = 2 [help_text = "Status for this Slice", default = True, null = False, db_index = False, blank = True];
-     required string description = 4 [help_text = "High level description of the slice and expected activities", max_length = 1024, null = False, db_index = False, blank = True, varchar = True];
+     optional string description = 4 [help_text = "High level description of the slice and expected activities", max_length = 1024, db_index = False, varchar = True];
      required manytoone site->Site:slices = 6:1005 [help_text = "The Site this Slice belongs to", null = False, db_index = True, blank = False];
      required int32 max_instances = 7 [default = 10, null = False, db_index = False, blank = False];
      optional manytoone service->Service:slices = 8:1006 [db_index = True, null = True, blank = True];