Merge branch 'master' of ssh://git.planet-lab.org/git/plstackapi
diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
index 1ca1307..3d2d7f7 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -399,7 +399,11 @@
 //        }
 
         if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) {
-            modelAttrs["validators"] = xosvalidators[modelName];
+            modelAttrs["validators"] = $.extend({}, xosvalidators[modelName], attrs["validators"] || {});
+        } else if (attrs["validators"]) {
+            modelAttrs["validators"] = attrs["validators"];
+            console.log(attrs);
+            console.log(modelAttrs);
         }
 
         lib[modelName] = XOSModel.extend(modelAttrs);
@@ -485,7 +489,7 @@
                            inputType: {"enabled": "checkbox"},
                            modelName: "slice",
                            xosValidate: function(attrs, options) {
-                               errors = XOSModel.prototype.xosValidate(this, attrs, options);
+                               errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
                                // validate that slice.name starts with site.login_base
                                site = attrs.site || this.site;
                                if ((site!=undefined) && (attrs.name!=undefined)) {
@@ -665,8 +669,9 @@
                            modelName: "slicePlus",
                            collectionName: "slicesPlus",
                            defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}),
+                           validators: {"network_ports": ["portspec"]},
                            xosValidate: function(attrs, options) {
-                               errors = XOSModel.prototype.xosValidate(this, attrs, options);
+                               errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
                                // validate that slice.name starts with site.login_base
                                site = attrs.site || this.site;
                                if ((site!=undefined) && (attrs.name!=undefined)) {
diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-util.js b/planetstack/core/xoslib/static/js/xoslib/xos-util.js
index 2fa38a6..04bd041 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-util.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-util.js
@@ -75,6 +75,12 @@
                 return "must be a valid url";
             }
             break;
+
+        case "portspec":
+            if (! $.trim(value).match(portlist_regexp())) {
+                return "must be a valid portspec (example: 'tcp 123, udp 456-789')"
+            }
+            break;
     }
 
     return true;
@@ -195,3 +201,47 @@
     }
     return nats
 }
+
+function portlist_regexp() {
+    /* this constructs the big complicated regexp that validates port
+       specifiers. Saved here in long form, in case we need to change it
+       in the future.
+    */
+
+    paren = function(x) { return "(?:" + x + ")"; }
+    whitespace = " *";
+    protocol = paren("tcp|udp");
+    protocolSlash = protocol + paren(whitespace + "|\/");
+    numbers = paren("[0-9]+");
+    range = paren(numbers + paren("-|:") + numbers);
+    numbersOrRange = paren(numbers + "|" + range);
+    protoPorts = paren(protocolSlash + numbersOrRange);
+    protoPortsCommas = paren(paren(protoPorts + "," + whitespace)+"+");
+    multiProtoPorts = paren(protoPortsCommas + protoPorts);
+    portSpec = "^" + paren(protoPorts + "|" + multiProtoPorts) + "$";
+    return RegExp(portSpec);
+}
+
+function portlist_selftest() {
+    r = portlist_regexp();
+    assert(! "tcp".match(r), 'should not have matched: "tcp"');
+    assert("tcp 1".match(r), 'should have matched: "tcp 1"');
+    assert("tcp 123".match(r), 'should have matched: "tcp 123"');
+    assert("tcp  123".match(r), 'should have matched: "tcp 123"');
+    assert("tcp 123-456".match(r), 'should have matched: "tcp 123-456"');
+    assert("tcp 123:456".match(r), 'should have matched: "tcp 123:456"');
+    assert(! "tcp 123-".match(r), 'should have matched: "tcp 123-"');
+    assert(! "tcp 123:".match(r), 'should have matched: "tcp 123:"');
+    assert(! "foo 123".match(r), 'should not have matched "foo 123"');
+    assert("udp 123".match(r), 'should have matched: "udp 123"');
+    assert("tcp 123,udp 456".match(r), 'should have matched: "tcp 123,udp 456"');
+    assert("tcp 123, udp 456".match(r), 'should have matched: "tcp 123, udp 456"');
+    assert("tcp 123,  udp 456".match(r), 'should have matched: "tcp 123,  udp 456"');
+    assert("tcp 123-45, udp 456".match(r), 'should have matched: "tcp 123-45, udp 456"');
+    assert("tcp 123-45, udp 456, tcp 11, tcp 22:45, udp 76, udp 47:49, udp 60-61".match(r), 'should have matched: "tcp 123-45, udp 456, tcp 11, tcp 22:45, udp 76, udp 47:49, udp 60-61"');
+    return "done";
+}
+
+//portlist_selftest();
+
+