[netemul] Environment configuration model

Classes for environment configuration parsing (placed in CMXMetadata ->
facets).

TEST: added unittests for model parsing
Change-Id: I5ecbf9edaa9df5b59fb765bd31813adb5767291d
diff --git a/bin/netemul_runner/model/BUILD.gn b/bin/netemul_runner/model/BUILD.gn
new file mode 100644
index 0000000..ec4b26a
--- /dev/null
+++ b/bin/netemul_runner/model/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("model") {
+  sources = [
+    "config.cc",
+    "config.h",
+    "endpoint.cc",
+    "endpoint.h",
+    "environment.cc",
+    "environment.h",
+    "launch_app.cc",
+    "launch_app.h",
+    "launch_service.cc",
+    "launch_service.h",
+    "network.cc",
+    "network.h",
+  ]
+
+  deps = [
+    "//garnet/public/lib/fsl",
+    "//garnet/public/lib/fxl",
+    "//garnet/public/lib/json:json",
+    "//garnet/public/lib/pkg_url",
+    "//third_party/rapidjson",
+  ]
+}
+
+executable("model_unittest") {
+  testonly = true
+  sources = [
+    "model_unittest.cc",
+  ]
+
+  deps = [
+    ":model",
+    "//garnet/public/lib/fxl/test:gtest_main",
+    "//garnet/public/lib/gtest",
+    "//garnet/public/lib/json",
+    "//garnet/public/lib/pkg_url",
+    "//third_party/rapidjson",
+  ]
+}
diff --git a/bin/netemul_runner/model/config.cc b/bin/netemul_runner/model/config.cc
new file mode 100644
index 0000000..5d634eb
--- /dev/null
+++ b/bin/netemul_runner/model/config.cc
@@ -0,0 +1,63 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "config.h"
+
+namespace netemul {
+namespace config {
+
+static const char* kNetworks = "networks";
+static const char* kEnvironment = "environment";
+
+const char Config::Facet[] = "fuchsia.netemul";
+
+bool Config::ParseFromJSON(const rapidjson::Value& value,
+                           json::JSONParser* json_parser) {
+  // null value keeps config as it is
+  if (value.IsNull()) {
+    return true;
+  }
+
+  if (!value.IsObject()) {
+    json_parser->ReportError("fuchsia.netemul object must be an Object");
+    return false;
+  }
+
+  auto nets_value = value.FindMember(kNetworks);
+  if (nets_value != value.MemberEnd()) {
+    if (!nets_value->value.IsArray()) {
+      json_parser->ReportError("\"networks\" property must be an Array");
+      return false;
+    }
+    const auto& nets = nets_value->value.GetArray();
+    for (auto n = nets.Begin(); n != nets.End(); n++) {
+      auto& net = networks_.emplace_back();
+      if (!net.ParseFromJSON(*n, json_parser)) {
+        return false;
+      }
+    }
+  }
+
+  auto env_value = value.FindMember(kEnvironment);
+  if (env_value == value.MemberEnd()) {
+    // parse from empty object if not present
+    if (!environment_.ParseFromJSON(rapidjson::Value(rapidjson::kObjectType),
+                                    json_parser)) {
+      return false;
+    }
+  } else {
+    if (!environment_.ParseFromJSON(env_value->value, json_parser)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+const std::vector<Network>& Config::networks() const { return networks_; }
+
+const Environment& Config::environment() const { return environment_; }
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/config.h b/bin/netemul_runner/model/config.h
new file mode 100644
index 0000000..71ec3eb
--- /dev/null
+++ b/bin/netemul_runner/model/config.h
@@ -0,0 +1,39 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_CONFIG_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_CONFIG_H_
+
+#include <vector>
+#include "environment.h"
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+#include "network.h"
+
+namespace netemul {
+namespace config {
+
+class Config {
+ public:
+  Config() = default;
+  Config(Config&& other) = default;
+
+  static const char Facet[];
+  bool ParseFromJSON(const rapidjson::Value& value,
+                     json::JSONParser* json_parser);
+
+  const std::vector<Network>& networks() const;
+
+  const Environment& environment() const;
+
+ private:
+  std::vector<Network> networks_;
+  Environment environment_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(Config);
+};
+
+}  // namespace config
+}  // namespace netemul
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_CONFIG_H_
diff --git a/bin/netemul_runner/model/endpoint.cc b/bin/netemul_runner/model/endpoint.cc
new file mode 100644
index 0000000..5f9922b
--- /dev/null
+++ b/bin/netemul_runner/model/endpoint.cc
@@ -0,0 +1,93 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "endpoint.h"
+#include <cstdio>
+#include <memory>
+
+namespace netemul {
+namespace config {
+
+static const char* kName = "name";
+static const char* kMtu = "mtu";
+static const char* kMac = "mac";
+static const char* kUp = "up";
+static const bool kDefaultUp = true;
+static const uint16_t kDefaultMtu = 1500;
+
+bool Endpoint::ParseFromJSON(const rapidjson::Value& value,
+                             json::JSONParser* parser) {
+  if (!value.IsObject()) {
+    parser->ReportError("endpoint must be object type");
+    return false;
+  }
+
+  auto name = value.FindMember(kName);
+  if (name == value.MemberEnd()) {
+    parser->ReportError("endpoint must have name property");
+    return false;
+  } else if ((!name->value.IsString()) || name->value.GetStringLength() == 0) {
+    parser->ReportError("endpoint name must be a non-empty string");
+    return false;
+  } else {
+    name_ = name->value.GetString();
+  }
+
+  auto mtu = value.FindMember(kMtu);
+  if (mtu == value.MemberEnd()) {
+    mtu_ = kDefaultMtu;
+  } else if (!mtu->value.IsNumber()) {
+    parser->ReportError("endpoint mtu must be number");
+    return false;
+  } else {
+    auto v = static_cast<uint16_t>(mtu->value.GetUint());
+    if (v == 0) {
+      parser->ReportError(
+          "endpoint with zero mtu is invalid, omit to use default");
+      return false;
+    }
+    mtu_ = v;
+  }
+
+  auto mac = value.FindMember(kMac);
+  if (mac == value.MemberEnd()) {
+    mac_ = nullptr;
+  } else if (!mac->value.IsString()) {
+    parser->ReportError("endpoint mac must be string");
+    return false;
+  } else {
+    auto macval = std::make_unique<Mac>();
+    if (std::sscanf(mac->value.GetString(),
+                    "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", macval->d,
+                    macval->d + 1, macval->d + 2, macval->d + 3, macval->d + 4,
+                    macval->d + 5) != 6) {
+      parser->ReportError("Can't parse supplied mac address");
+      return false;
+    }
+    mac_ = std::move(macval);
+  }
+
+  auto up = value.FindMember(kUp);
+  if (up == value.MemberEnd()) {
+    up_ = kDefaultUp;
+  } else if (!up->value.IsBool()) {
+    parser->ReportError("endpoint up must be bool");
+    return false;
+  } else {
+    up_ = up->value.GetBool();
+  }
+
+  return true;
+}
+
+const std::string& Endpoint::name() const { return name_; }
+
+const std::unique_ptr<Mac>& Endpoint::mac() const { return mac_; }
+
+uint16_t Endpoint::mtu() const { return mtu_; }
+
+bool Endpoint::up() const { return up_; }
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/endpoint.h b/bin/netemul_runner/model/endpoint.h
new file mode 100644
index 0000000..5d61f62
--- /dev/null
+++ b/bin/netemul_runner/model/endpoint.h
@@ -0,0 +1,41 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_ENDPOINT_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_ENDPOINT_H_
+
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+
+namespace netemul {
+namespace config {
+
+struct Mac {
+  uint8_t d[6];
+};
+
+class Endpoint {
+ public:
+  Endpoint() = default;
+  Endpoint(Endpoint&& other) = default;
+
+  bool ParseFromJSON(const rapidjson::Value& value, json::JSONParser* parser);
+
+  const std::string& name() const;
+  const std::unique_ptr<Mac>& mac() const;
+  uint16_t mtu() const;
+  bool up() const;
+
+ private:
+  std::string name_;
+  std::unique_ptr<Mac> mac_;
+  uint16_t mtu_;
+  bool up_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(Endpoint);
+};
+
+}  // namespace config
+}  // namespace netemul
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_ENDPOINT_H_
diff --git a/bin/netemul_runner/model/environment.cc b/bin/netemul_runner/model/environment.cc
new file mode 100644
index 0000000..3bc4a34
--- /dev/null
+++ b/bin/netemul_runner/model/environment.cc
@@ -0,0 +1,172 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "environment.h"
+
+namespace netemul {
+namespace config {
+
+static const char* kDefaultName = "test-env";
+static const char* kName = "name";
+static const char* kServices = "services";
+static const char* kDevices = "devices";
+static const char* kChildren = "children";
+static const char* kTest = "test";
+static const char* kInheritServices = "inherit_services";
+static const char* kApps = "apps";
+static const char* kSetup = "setup";
+static const bool kDefaultInheritServices = true;
+
+bool Environment::ParseFromJSON(const rapidjson::Value& value,
+                                json::JSONParser* parser) {
+  if (!value.IsObject()) {
+    parser->ReportError("environment must be object type");
+    return false;
+  }
+
+  auto name = value.FindMember(kName);
+  if (name == value.MemberEnd()) {
+    name_ = kDefaultName;
+  } else if (!name->value.IsString()) {
+    parser->ReportError("environment name must be string value");
+    return false;
+  } else {
+    name_ = name->value.GetString();
+  }
+
+  auto inherit = value.FindMember(kInheritServices);
+  if (inherit == value.MemberEnd()) {
+    inherit_services_ = kDefaultInheritServices;
+  } else if (!inherit->value.IsBool()) {
+    parser->ReportError("inherit_services must be boolean value");
+    return false;
+  } else {
+    inherit_services_ = inherit->value.GetBool();
+  }
+
+  auto devices = value.FindMember(kDevices);
+  if (devices == value.MemberEnd()) {
+    devices_.clear();
+  } else if (!devices->value.IsArray()) {
+    parser->ReportError("environment devices must be array of strings");
+    return false;
+  } else {
+    auto devs = devices->value.GetArray();
+    devices_.clear();
+    for (auto d = devs.Begin(); d != devs.End(); d++) {
+      if (!d->IsString()) {
+        parser->ReportError("environment devices must be array of strings");
+        return false;
+      }
+      devices_.emplace_back(d->GetString());
+    }
+  }
+
+  auto services = value.FindMember(kServices);
+  if (services == value.MemberEnd()) {
+    services_.clear();
+  } else if (!services->value.IsObject()) {
+    parser->ReportError("environment services must be object");
+    return false;
+  } else {
+    for (auto s = services->value.MemberBegin();
+         s != services->value.MemberEnd(); s++) {
+      auto& ns = services_.emplace_back(s->name.GetString());
+      if (!ns.ParseFromJSON(s->value, parser)) {
+        return false;
+      }
+    }
+  }
+
+  auto test = value.FindMember(kTest);
+  if (test == value.MemberEnd()) {
+    test_.clear();
+  } else if (!test->value.IsArray()) {
+    parser->ReportError("environment tests must be array of objects");
+    return false;
+  } else {
+    auto test_arr = test->value.GetArray();
+    for (auto t = test_arr.Begin(); t != test_arr.End(); t++) {
+      auto& nt = test_.emplace_back();
+      if (!nt.ParseFromJSON(*t, parser)) {
+        return false;
+      }
+    }
+  }
+
+  auto children = value.FindMember(kChildren);
+  if (children == value.MemberEnd()) {
+    children_.clear();
+  } else if (!children->value.IsArray()) {
+    parser->ReportError("environment children must be array of objects");
+    return false;
+  } else {
+    auto ch_arr = children->value.GetArray();
+    for (auto c = ch_arr.Begin(); c != ch_arr.End(); c++) {
+      auto& nc = children_.emplace_back();
+      if (!nc.ParseFromJSON(*c, parser)) {
+        return false;
+      }
+    }
+  }
+
+  auto apps = value.FindMember(kApps);
+  if (apps == value.MemberEnd()) {
+    apps_.clear();
+  } else if (!apps->value.IsArray()) {
+    parser->ReportError("environment apps must be array");
+    return false;
+  } else {
+    auto app_arr = apps->value.GetArray();
+    for (auto a = app_arr.Begin(); a != app_arr.End(); a++) {
+      auto& na = apps_.emplace_back();
+      if (!na.ParseFromJSON(*a, parser)) {
+        return false;
+      }
+    }
+  }
+
+  auto setup = value.FindMember(kSetup);
+  if (setup == value.MemberEnd()) {
+    setup_.clear();
+  } else if (!setup->value.IsArray()) {
+    parser->ReportError("environment setup must be array");
+    return false;
+  } else {
+    auto setup_arr = setup->value.GetArray();
+    for (auto s = setup_arr.Begin(); s != setup_arr.End(); s++) {
+      auto& ns = setup_.emplace_back();
+      if (!ns.ParseFromJSON(*s, parser)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+const std::string& Environment::name() const { return name_; }
+
+const std::vector<Environment>& Environment::children() const {
+  return children_;
+}
+
+const std::vector<std::string>& Environment::devices() const {
+  return devices_;
+}
+
+const std::vector<LaunchService>& Environment::services() const {
+  return services_;
+}
+
+const std::vector<LaunchApp>& Environment::test() const { return test_; }
+
+bool Environment::inherit_services() const { return inherit_services_; }
+
+const std::vector<LaunchApp>& Environment::apps() const { return apps_; }
+
+const std::vector<LaunchApp>& Environment::setup() const { return setup_; }
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/environment.h b/bin/netemul_runner/model/environment.h
new file mode 100644
index 0000000..bd827d8
--- /dev/null
+++ b/bin/netemul_runner/model/environment.h
@@ -0,0 +1,47 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_ENVIRONMENT_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_ENVIRONMENT_H_
+
+#include "launch_app.h"
+#include "launch_service.h"
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+
+namespace netemul {
+namespace config {
+
+class Environment {
+ public:
+  Environment() = default;
+  Environment(Environment&& other) = default;
+
+  bool ParseFromJSON(const rapidjson::Value& value, json::JSONParser* parser);
+
+  const std::string& name() const;
+  const std::vector<Environment>& children() const;
+  const std::vector<std::string>& devices() const;
+  const std::vector<LaunchService>& services() const;
+  const std::vector<LaunchApp>& test() const;
+  const std::vector<LaunchApp>& apps() const;
+  const std::vector<LaunchApp>& setup() const;
+  bool inherit_services() const;
+
+ private:
+  std::string name_;
+  std::vector<Environment> children_;
+  std::vector<std::string> devices_;
+  std::vector<LaunchService> services_;
+  std::vector<LaunchApp> test_;
+  std::vector<LaunchApp> apps_;
+  std::vector<LaunchApp> setup_;
+  bool inherit_services_{};
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(Environment);
+};
+
+}  // namespace config
+}  // namespace netemul
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_ENVIRONMENT_H_
diff --git a/bin/netemul_runner/model/launch_app.cc b/bin/netemul_runner/model/launch_app.cc
new file mode 100644
index 0000000..2e691fb
--- /dev/null
+++ b/bin/netemul_runner/model/launch_app.cc
@@ -0,0 +1,91 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "launch_app.h"
+#include "lib/pkg_url/fuchsia_pkg_url.h"
+
+namespace netemul {
+namespace config {
+
+static const char* kUrl = "url";
+static const char* kArguments = "arguments";
+static const char* kEmptyUrl = "";
+
+bool LaunchApp::ParseFromJSON(const rapidjson::Value& value,
+                              json::JSONParser* parser) {
+  if (value.IsString()) {
+    // value is a string, parse as url only
+    auto url = value.GetString();
+    if (value.GetStringLength() != 0) {
+      component::FuchsiaPkgUrl pkgUrl;
+      if (!pkgUrl.Parse(url)) {
+        parser->ReportError(
+            "launch options url is not a valid fuchsia package url");
+        return false;
+      }
+    }
+    url_ = url;
+    arguments_.clear();
+
+  } else if (value.IsObject()) {
+    auto url = value.FindMember(kUrl);
+    if (url == value.MemberEnd()) {
+      url_ = kEmptyUrl;
+    } else if (!url->value.IsString()) {
+      parser->ReportError("launch options url must be string");
+      return false;
+    } else {
+      auto v = url->value.GetString();
+      if (url->value.GetStringLength() != 0) {
+        component::FuchsiaPkgUrl pkgUrl;
+        if (!pkgUrl.Parse(v)) {
+          parser->ReportError(
+              "launch options url is not a valid fuchsia package url");
+          return false;
+        }
+      }
+      url_ = v;
+    }
+
+    auto arguments = value.FindMember(kArguments);
+    if (arguments == value.MemberEnd()) {
+      arguments_.clear();
+    } else if (!arguments->value.IsArray()) {
+      parser->ReportError("launch options arguments must be array of string");
+      return false;
+    } else {
+      auto arg_arr = arguments->value.GetArray();
+      for (auto a = arg_arr.Begin(); a != arg_arr.End(); a++) {
+        if (!a->IsString()) {
+          parser->ReportError(
+              "launch options arguments element must be string");
+          return false;
+        }
+        arguments_.emplace_back(a->GetString());
+      }
+    }
+  } else {
+    parser->ReportError("launch options must be of type object or string");
+    return false;
+  }
+
+  return true;
+}
+
+const std::string& LaunchApp::url() const { return url_; }
+
+const std::vector<std::string>& LaunchApp::arguments() const {
+  return arguments_;
+}
+
+const std::string& LaunchApp::GetUrlOrDefault(const std::string& def) const {
+  if (url_.empty()) {
+    return def;
+  } else {
+    return url_;
+  }
+}
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/launch_app.h b/bin/netemul_runner/model/launch_app.h
new file mode 100644
index 0000000..e6fbdc1
--- /dev/null
+++ b/bin/netemul_runner/model/launch_app.h
@@ -0,0 +1,35 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_APP_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_APP_H_
+
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+
+namespace netemul {
+namespace config {
+
+class LaunchApp {
+ public:
+  LaunchApp() = default;
+  LaunchApp(LaunchApp&& other) = default;
+
+  bool ParseFromJSON(const rapidjson::Value& value, json::JSONParser* parser);
+
+  const std::string& GetUrlOrDefault(const std::string& def) const;
+
+  const std::string& url() const;
+  const std::vector<std::string>& arguments() const;
+
+ private:
+  std::string url_;
+  std::vector<std::string> arguments_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(LaunchApp);
+};
+
+}  // namespace config
+}  // namespace netemul
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_APP_H_
diff --git a/bin/netemul_runner/model/launch_service.cc b/bin/netemul_runner/model/launch_service.cc
new file mode 100644
index 0000000..c5833d4
--- /dev/null
+++ b/bin/netemul_runner/model/launch_service.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "launch_service.h"
+#include "lib/pkg_url/fuchsia_pkg_url.h"
+
+namespace netemul {
+namespace config {
+
+LaunchService::LaunchService(std::string name) : name_(std::move(name)) {}
+
+bool LaunchService::ParseFromJSON(const rapidjson::Value& value,
+                                  json::JSONParser* parser) {
+  return launch_.ParseFromJSON(value, parser);
+}
+
+const std::string& LaunchService::name() const { return name_; }
+
+const LaunchApp& LaunchService::launch() const { return launch_; }
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/launch_service.h b/bin/netemul_runner/model/launch_service.h
new file mode 100644
index 0000000..a5fd4ed
--- /dev/null
+++ b/bin/netemul_runner/model/launch_service.h
@@ -0,0 +1,35 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_SERVICE_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_SERVICE_H_
+
+#include "launch_app.h"
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+
+namespace netemul {
+namespace config {
+
+class LaunchService {
+ public:
+  explicit LaunchService(std::string name);
+  LaunchService(LaunchService&& other) = default;
+
+  bool ParseFromJSON(const rapidjson::Value& value, json::JSONParser* parser);
+
+  const std::string& name() const;
+  const LaunchApp& launch() const;
+
+ private:
+  std::string name_;
+  LaunchApp launch_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(LaunchService);
+};
+
+}  // namespace config
+}  // namespace netemul
+
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_LAUNCH_SERVICE_H_
diff --git a/bin/netemul_runner/model/model_unittest.cc b/bin/netemul_runner/model/model_unittest.cc
new file mode 100644
index 0000000..d96a2a9
--- /dev/null
+++ b/bin/netemul_runner/model/model_unittest.cc
@@ -0,0 +1,276 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <iostream>
+#include "config.h"
+#include "gtest/gtest.h"
+
+namespace netemul {
+namespace config {
+namespace testing {
+
+class ModelTest : public ::testing::Test {
+ protected:
+  void ExpectFailedParse(const char* json, const char* msg) {
+    Config config;
+    json::JSONParser parser;
+    auto doc = parser.ParseFromString(
+        json, ::testing::UnitTest::GetInstance()->current_test_info()->name());
+
+    ASSERT_FALSE(parser.HasError());
+
+    EXPECT_FALSE(config.ParseFromJSON(doc, &parser)) << msg;
+    ASSERT_TRUE(parser.HasError());
+    std::cout << "Parse failed as expected: " << parser.error_str()
+              << std::endl;
+  }
+};
+
+TEST_F(ModelTest, ParseTest) {
+  const char* json =
+      R"(
+    {
+      "environment": {
+        "children": [
+          {
+            "name": "child-1",
+            "test": [
+              {
+                "arguments": [
+                  "-t",
+                  "1",
+                  "-n",
+                  "child-1-url"
+                ],
+                "url": "fuchsia-pkg://fuchsia.com/netemul_sandbox_test#meta/env_build_run.cmx"
+              },
+              {
+                "arguments": [
+                  "-t",
+                  "1",
+                  "-n",
+                  "child-1-no-url"
+                ]
+              }
+            ]
+          },
+          {
+            "inherit_services": false,
+            "name": "child-2",
+            "test": [ "fuchsia-pkg://fuchsia.com/some_test#meta/some_test.cmx" ],
+            "apps" : [ "fuchsia-pkg://fuchsia.com/some_app#meta/some_app.cmx" ]
+          }
+        ],
+        "devices": [
+          "ep0",
+          "ep1"
+        ],
+        "name": "root",
+        "setup": [
+          {
+            "url": "fuchsia-pkg://fuchsia.com/some_setup#meta/some_setup.cmx",
+            "arguments": ["-arg"]
+          }
+        ],
+        "services": {
+          "fuchsia.net.SocketProvider": "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx",
+          "fuchsia.netstack.Netstack": "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx",
+          "fuchsia.some.Service" : {
+            "url" : "fuchsia-pkg://fuchsia.com/some_service#meta/some_service.cmx",
+            "arguments" : ["-a1", "-a2"]
+          }
+        }
+      },
+      "networks": [
+        {
+          "endpoints": [
+            {
+              "mac": "70:00:01:02:03:04",
+              "mtu": 1000,
+              "name": "ep0",
+              "up": false
+            },
+            {
+              "name": "ep1"
+            }
+          ],
+          "name": "test-net"
+        }
+      ]
+    }
+)";
+
+  config::Config config;
+  json::JSONParser parser;
+  auto doc = parser.ParseFromString(json, "ParseTest");
+  EXPECT_FALSE(parser.HasError()) << "Parse error: " << parser.error_str();
+
+  EXPECT_TRUE(config.ParseFromJSON(doc, &parser))
+      << "Parse error: " << parser.error_str();
+
+  // sanity check the objects:
+  auto& root_env = config.environment();
+  EXPECT_EQ(root_env.name(), "root");
+  EXPECT_EQ(root_env.inherit_services(), true);
+  EXPECT_EQ(root_env.children().size(), 2ul);
+  EXPECT_EQ(root_env.devices().size(), 2ul);
+  EXPECT_EQ(root_env.services().size(), 3ul);
+  EXPECT_TRUE(root_env.test().empty());
+  EXPECT_TRUE(root_env.apps().empty());
+  EXPECT_EQ(root_env.setup().size(), 1ul);
+
+  // check the devices
+  EXPECT_EQ(root_env.devices()[0], "ep0");
+  EXPECT_EQ(root_env.devices()[1], "ep1");
+
+  // check the services
+  EXPECT_EQ(root_env.services()[0].name(), "fuchsia.net.SocketProvider");
+  EXPECT_EQ(root_env.services()[0].launch().url(),
+            "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx");
+  EXPECT_TRUE(root_env.services()[0].launch().arguments().empty());
+  EXPECT_EQ(root_env.services()[1].name(), "fuchsia.netstack.Netstack");
+  EXPECT_EQ(root_env.services()[1].launch().url(),
+            "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx");
+  EXPECT_TRUE(root_env.services()[1].launch().arguments().empty());
+  EXPECT_EQ(root_env.services()[2].name(), "fuchsia.some.Service");
+  EXPECT_EQ(root_env.services()[2].launch().url(),
+            "fuchsia-pkg://fuchsia.com/some_service#meta/some_service.cmx");
+  EXPECT_EQ(root_env.services()[2].launch().arguments().size(), 2ul);
+
+  // check the child environments
+  auto& c0 = root_env.children()[0];
+  EXPECT_EQ(c0.name(), "child-1");
+  EXPECT_EQ(c0.inherit_services(), true);
+  EXPECT_TRUE(c0.children().empty());
+  EXPECT_TRUE(c0.devices().empty());
+  EXPECT_TRUE(c0.services().empty());
+  EXPECT_EQ(c0.test().size(), 2ul);
+  EXPECT_TRUE(c0.apps().empty());
+  EXPECT_TRUE(c0.setup().empty());
+  auto& c1 = root_env.children()[1];
+  EXPECT_EQ(c1.name(), "child-2");
+  EXPECT_EQ(c1.inherit_services(), false);
+  EXPECT_TRUE(c1.children().empty());
+  EXPECT_TRUE(c1.devices().empty());
+  EXPECT_TRUE(c1.services().empty());
+  EXPECT_TRUE(c1.setup().empty());
+  EXPECT_EQ(c1.test().size(), 1ul);
+  EXPECT_EQ(c1.apps().size(), 1ul);
+
+  // check test structures:
+  auto& t0 = c0.test()[0];
+  auto& t1 = c0.test()[1];
+  auto& t2 = c1.test()[0];
+  EXPECT_EQ(
+      t0.url(),
+      "fuchsia-pkg://fuchsia.com/netemul_sandbox_test#meta/env_build_run.cmx");
+  EXPECT_EQ(t0.arguments().size(), 4ul);
+
+  EXPECT_TRUE(t1.url().empty());
+  EXPECT_EQ(t1.arguments().size(), 4ul);
+  EXPECT_EQ(t2.url(), "fuchsia-pkg://fuchsia.com/some_test#meta/some_test.cmx");
+  EXPECT_TRUE(t2.arguments().empty());
+
+  // check apps:
+  auto& app0 = c1.apps()[0];
+  EXPECT_EQ(app0.url(), "fuchsia-pkg://fuchsia.com/some_app#meta/some_app.cmx");
+  EXPECT_TRUE(app0.arguments().empty());
+
+  // check setup:
+  auto& setup = root_env.setup()[0];
+  EXPECT_EQ(setup.url(),
+            "fuchsia-pkg://fuchsia.com/some_setup#meta/some_setup.cmx");
+  EXPECT_EQ(setup.arguments().size(), 1ul);
+  EXPECT_EQ(setup.arguments()[0], "-arg");
+
+  // check network object:
+  EXPECT_EQ(config.networks().size(), 1ul);
+  auto& net = config.networks()[0];
+  EXPECT_EQ(net.name(), "test-net");
+  EXPECT_EQ(net.endpoints().size(), 2ul);
+
+  // check endpoints:
+  auto& ep0 = net.endpoints()[0];
+  auto& ep1 = net.endpoints()[1];
+  EXPECT_EQ(ep0.name(), "ep0");
+  EXPECT_EQ(ep0.mtu(), 1000u);
+  EXPECT_TRUE(ep0.mac());
+  const uint8_t mac_cmp[] = {0x70, 0x00, 0x01, 0x02, 0x03, 0x04};
+  EXPECT_EQ(memcmp(mac_cmp, ep0.mac()->d, sizeof(mac_cmp)), 0);
+  EXPECT_EQ(ep0.up(), false);
+
+  EXPECT_EQ(ep1.name(), "ep1");
+  EXPECT_EQ(ep1.mtu(), 1500u);  // default mtu check
+  EXPECT_FALSE(ep1.mac());      // mac not set
+  EXPECT_EQ(ep1.up(), true);    // default up
+}
+
+TEST_F(ModelTest, NetworkNoName) {
+  const char* json = R"({"networks":[{}]})";
+  ExpectFailedParse(json, "network without name accepted");
+
+  const char* json2 = R"({"networks":[{"name":""}]})";
+  ExpectFailedParse(json2, "network with empty name accepted");
+};
+
+TEST_F(ModelTest, EndpointNoName) {
+  const char* json = R"({"networks":[{"name":"net","endpoints":[{}]}]})";
+  ExpectFailedParse(json, "endpoint without name accepted");
+
+  const char* json2 =
+      R"({"networks":[{"name":"net","endpoints":[{"name":""}]}]})";
+  ExpectFailedParse(json2, "endpoint with empty name accepted");
+};
+
+TEST_F(ModelTest, EndpointBadMtu) {
+  const char* json =
+      R"({"networks":[{"name":"net","endpoints":[{"name":"a","mtu":0}]}]})";
+  ExpectFailedParse(json, "endpoint without 0 mtu accepted");
+}
+
+TEST_F(ModelTest, EndpointBadMac) {
+  const char* json =
+      R"({"networks":[{"name":"net","endpoints":[{"name":"a","mac":"xx:xx:xx"}]}]})";
+  ExpectFailedParse(json, "endpoint with invalid mac accepted");
+}
+
+TEST_F(ModelTest, TestBadUrl) {
+  const char* json = R"({"environment":{"test":[{"url":"blablabla"}]}})";
+  ExpectFailedParse(json, "test with bad url accepted");
+}
+
+TEST_F(ModelTest, ServiceBadUrl) {
+  const char* json =
+      R"({"environment":{"services":{"some.service":"blablabla"}}})";
+  ExpectFailedParse(json, "service with bad url accepted");
+}
+
+TEST_F(ModelTest, LaunchAppGetOrDefault) {
+  const char* json1 =
+      R"({"url":"fuchsia-pkg://fuchsia.com/some_url#meta/some_url.cmx"})";
+  json::JSONParser parser;
+  auto doc1 = parser.ParseFromString(json1, "LaunchApGetOrDefault");
+  config::LaunchApp app1;
+
+  EXPECT_FALSE(parser.HasError()) << "Parse error: " << parser.error_str();
+  EXPECT_TRUE(app1.ParseFromJSON(doc1, &parser))
+      << "Parse error: " << parser.error_str();
+
+  const char* json2 = R"({"url":""})";
+  auto doc2 = parser.ParseFromString(json2, "LaunchApGetOrDefault");
+
+  config::LaunchApp app2;
+  EXPECT_FALSE(parser.HasError()) << "Parse error: " << parser.error_str();
+  EXPECT_TRUE(app2.ParseFromJSON(doc2, &parser))
+      << "Parse error: " << parser.error_str();
+
+  const char* fallback = "fuchsia-pkg://fuchsia.com/fallback#meta/fallback.cmx";
+  EXPECT_EQ(app1.GetUrlOrDefault(fallback),
+            "fuchsia-pkg://fuchsia.com/some_url#meta/some_url.cmx");
+  EXPECT_EQ(app2.GetUrlOrDefault(fallback), fallback);
+}
+
+}  // namespace testing
+}  // namespace config
+}  // namespace netemul
diff --git a/bin/netemul_runner/model/network.cc b/bin/netemul_runner/model/network.cc
new file mode 100644
index 0000000..7248c88
--- /dev/null
+++ b/bin/netemul_runner/model/network.cc
@@ -0,0 +1,55 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "network.h"
+
+namespace netemul {
+namespace config {
+
+static const char* kName = "name";
+static const char* kEndpoints = "endpoints";
+
+bool Network::ParseFromJSON(const rapidjson::Value& value,
+                            json::JSONParser* json_parser) {
+  if (!value.IsObject()) {
+    json_parser->ReportError("network entry must be an object");
+    return false;
+  }
+
+  auto name = value.FindMember(kName);
+  if (name == value.MemberEnd()) {
+    json_parser->ReportError("network must have name property set");
+    return false;
+  } else if ((!name->value.IsString()) || name->value.GetStringLength() == 0) {
+    json_parser->ReportError("network name must be a non-empty string");
+    return false;
+  } else {
+    name_ = name->value.GetString();
+  }
+
+  auto endpoints = value.FindMember(kEndpoints);
+  if (endpoints == value.MemberEnd()) {
+    endpoints_.clear();
+  } else if (!endpoints->value.IsArray()) {
+    json_parser->ReportError("network endpoints must be an array");
+    return false;
+  } else {
+    auto eps = endpoints->value.GetArray();
+    for (auto e = eps.Begin(); e != eps.End(); e++) {
+      auto& ne = endpoints_.emplace_back();
+      if (!ne.ParseFromJSON(*e, json_parser)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+const std::string& Network::name() const { return name_; }
+
+const std::vector<Endpoint>& Network::endpoints() const { return endpoints_; }
+
+}  // namespace config
+}  // namespace netemul
\ No newline at end of file
diff --git a/bin/netemul_runner/model/network.h b/bin/netemul_runner/model/network.h
new file mode 100644
index 0000000..69fb949
--- /dev/null
+++ b/bin/netemul_runner/model/network.h
@@ -0,0 +1,35 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_NETEMUL_RUNNER_MODEL_NETWORK_H_
+#define GARNET_BIN_NETEMUL_RUNNER_MODEL_NETWORK_H_
+
+#include "endpoint.h"
+#include "lib/fxl/macros.h"
+#include "lib/json/json_parser.h"
+
+namespace netemul {
+namespace config {
+
+class Network {
+ public:
+  Network() = default;
+  Network(Network&& other) = default;
+
+  bool ParseFromJSON(const rapidjson::Value& value,
+                     json::JSONParser* json_parser);
+
+  const std::string& name() const;
+  const std::vector<Endpoint>& endpoints() const;
+
+ private:
+  std::string name_;
+  std::vector<Endpoint> endpoints_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(Network);
+};
+
+}  // namespace config
+}  // namespace netemul
+#endif  // GARNET_BIN_NETEMUL_RUNNER_MODEL_NETWORK_H_
diff --git a/bin/netemul_runner/test/BUILD.gn b/bin/netemul_runner/test/BUILD.gn
index 76c1ab6..e9ad883 100644
--- a/bin/netemul_runner/test/BUILD.gn
+++ b/bin/netemul_runner/test/BUILD.gn
@@ -6,6 +6,7 @@
 
 test_package("netemul_sandbox_test") {
   deps = [
+    "//garnet/bin/netemul_runner/model:model_unittest",
     "//garnet/bin/netemul_runner/test/netstack_socks",
     "//garnet/bin/netemul_runner/test/svc_list",
   ]
@@ -28,5 +29,8 @@
     {
       name = "netstack_socks"
     },
+    {
+      name = "model_unittest"
+    },
   ]
 }
diff --git a/bin/netemul_runner/test/meta/model_unittest.cmx b/bin/netemul_runner/test/meta/model_unittest.cmx
new file mode 100644
index 0000000..44fcfdf
--- /dev/null
+++ b/bin/netemul_runner/test/meta/model_unittest.cmx
@@ -0,0 +1,5 @@
+{
+    "program": {
+        "binary": "test/model_unittest"
+    }
+}