Repository: replicatedhq/troubleshoot
Pull request #311 (2021-02-08 17:33:57)

Host preflight

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/replicatedhq/troubleshoot?pullRequest=311) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our [Contributor License Agreement](https://cla-assistant.io/replicatedhq/troubleshoot?pullRequest=311) before we can accept your contribution.<br/>**1** out of **2** committers have signed the CLA.<br/><br/>:white_check_mark: marccampbell<br/>:x: areed<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/replicatedhq/troubleshoot?pullRequest=311) it.</sub>

Patch:
diff --git a/cmd/preflight/cli/run.go b/cmd/preflight/cli/run.go
index 6fb7136f..5a3dc58c 100644
--- a/cmd/preflight/cli/run.go
+++ b/cmd/preflight/cli/run.go
@@ -86,15 +86,16 @@ func runPreflights(v *viper.Viper, arg string) error {
 		return errors.Wrapf(err, "failed to parse %s", arg)
 	}
 
-	preflightSpec := obj.(*troubleshootv1beta2.Preflight)
+	var collectResults preflight.CollectResult
+	preflightSpecName := ""
+	finishedCh := make(chan bool, 1)
+	progressCh := make(chan interface{}, 0) // non-zero buffer will result in missed messages
 
 	s := spin.New()
-	finishedCh := make(chan bool, 1)
-	progressChan := make(chan interface{}, 0) // non-zero buffer will result in missed messages
 	go func() {
 		for {
 			select {
-			case msg, ok := <-progressChan:
+			case msg, ok := <-progressCh:
 				if !ok {
 					continue
 				}
@@ -114,60 +115,105 @@ func runPreflights(v *viper.Viper, arg string) error {
 			}
 		}
 	}()
+
 	defer func() {
 		close(finishedCh)
+		close(progressCh)
 	}()
 
+	if preflightSpec, ok := obj.(*troubleshootv1beta2.Preflight); ok {
+		r, err := collectInCluster(preflightSpec, finishedCh, progressCh)
+		if err != nil {
+			return errors.Wrap(err, "failed to collect in cluster")
+		}
+		collectResults = *r
+		preflightSpecName = preflightSpec.Name
+	} else if hostPreflightSpec, ok := obj.(*troubleshootv1beta2.HostPreflight); ok {
+		r, err := collectHost(hostPreflightSpec, finishedCh, progressCh)
+		if err != nil {
+			return errors.Wrap(err, "failed to collect from host")
+		}
+		collectResults = *r
+		preflightSpecName = hostPreflightSpec.Name
+	}
+
+	if collectResults == nil {
+		return errors.New("no results")
+	}
+
+	analyzeResults := collectResults.Analyze()
+
+	if preflightSpec, ok := obj.(*troubleshootv1beta2.Preflight); ok {
+		if preflightSpec.Spec.UploadResultsTo != "" {
+			err := uploadResults(preflightSpec.Spec.UploadResultsTo, analyzeResults)
+			if err != nil {
+				progressCh <- err
+			}
+		}
+	}
+
+	finishedCh <- true
+
+	if v.GetBool("interactive") {
+		if len(analyzeResults) == 0 {
+			return errors.New("no data has been collected")
+		}
+		return showInteractiveResults(preflightSpecName, analyzeResults)
+	}
+
+	return showStdoutResults(v.GetString("format"), preflightSpecName, analyzeResults)
+}
+
+func collectInCluster(preflightSpec *troubleshootv1beta2.Preflight, finishedCh chan bool, progressCh chan interface{}) (*preflight.CollectResult, error) {
+	v := viper.GetViper()
+
 	restConfig, err := k8sutil.GetRESTConfig()
 	if err != nil {
-		return errors.Wrap(err, "failed to convert kube flags to rest config")
+		return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
 	}
 
 	collectOpts := preflight.CollectOpts{
 		Namespace:              v.GetString("namespace"),
 		IgnorePermissionErrors: v.GetBool("collect-without-permissions"),
-		ProgressChan:           progressChan,
+		ProgressChan:           progressCh,
 		KubernetesRestConfig:   restConfig,
 	}
 
 	if v.GetString("since") != "" || v.GetString("since-time") != "" {
-		err := parseTimeFlags(v, progressChan, preflightSpec.Spec.Collectors)
+		err := parseTimeFlags(v, progressCh, preflightSpec.Spec.Collectors)
 		if err != nil {
-			return err
+			return nil, err
 		}
 	}
 
 	collectResults, err := preflight.Collect(collectOpts, preflightSpec)
 	if err != nil {
-		if !collectResults.IsRBACAllowed {
+		if !collectResults.IsRBACAllowed() {
 			if preflightSpec.Spec.UploadResultsTo != "" {
-				err := uploadErrors(preflightSpec.Spec.UploadResultsTo, collectResults.Collectors)
+				clusterCollectResults := collectResults.(preflight.ClusterCollectResult)
+				err := uploadErrors(preflightSpec.Spec.UploadResultsTo, clusterCollectResults.Collectors)
 				if err != nil {
-					progressChan <- err
+					progressCh <- err
 				}
 			}
 		}
-		return err
+		return nil, err
 	}
 
-	analyzeResults := collectResults.Analyze()
-	if preflightSpec.Spec.UploadResultsTo != "" {
-		err := uploadResults(preflightSpec.Spec.UploadResultsTo, analyzeResults)
-		if err != nil {
-			progressChan <- err
-		}
-	}
+	return &collectResults, nil
+}
 
-	finishedCh <- true
+func collectHost(hostPreflightSpec *troubleshootv1beta2.HostPreflight, finishedCh chan bool, progressCh chan interface{}) (*preflight.CollectResult, error) {
+	collectOpts := preflight.CollectOpts{
+		ProgressChan: progressCh,
+	}
 
-	if v.GetBool("interactive") {
-		if len(analyzeResults) == 0 {
-			return errors.New("no data has been collected")
-		}
-		return showInteractiveResults(preflightSpec.Name, analyzeResults)
+	collectResults, err := preflight.CollectHost(collectOpts, hostPreflightSpec)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to collect from host")
 	}
 
-	return showStdoutResults(v.GetString("format"), preflightSpec.Name, analyzeResults)
+	return &collectResults, nil
 }
 
 func parseTimeFlags(v *viper.Viper, progressChan chan interface{}, collectors []*troubleshootv1beta2.Collect) error {
diff --git a/examples/preflight/host-cpu.yaml b/examples/preflight/host-cpu.yaml
new file mode 100644
index 00000000..1cd09aae
--- /dev/null
+++ b/examples/preflight/host-cpu.yaml
@@ -0,0 +1,22 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: cpu
+spec:
+  collectors:
+    - cpu: {}
+  analyzers:
+    - cpu:
+        outcomes:
+          - fail:
+              when: "physical < 4"
+              message: At least 4 physical CPU cores are required
+          - fail:
+              when: "logical < 8"
+              message: At least 8 CPU cores are required
+          - warn:
+              when: "count < 16"
+              message: At least 16 CPU cores preferred
+          - pass:
+              message: This server has sufficient CPU cores
+
diff --git a/examples/preflight/host-disk-usage.yaml b/examples/preflight/host-disk-usage.yaml
new file mode 100644
index 00000000..abd8b3d6
--- /dev/null
+++ b/examples/preflight/host-disk-usage.yaml
@@ -0,0 +1,33 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: diskUsage
+spec:
+  collectors:
+    - diskUsage:
+        collectorName: ephemeral
+        path: /var/lib/kubelet
+  analyzers:
+    - diskUsage:
+        collectorName: ephemeral
+        outcomes:
+          - fail:
+              when: "total < 20Gi"
+              message: /var/lib/kubelet has less than 20Gi of total space
+          - fail:
+              when: "available < 10Gi"
+              message: /var/lib/kubelet has less than 10Gi of disk space available
+          - fail:
+              when: "used/total > 70%"
+              message: /var/lib/kubelet is more than 70% full
+          - warn:
+              when: "total < 40Gi"
+              message: /var/lib/kubelet has less than 40Gi of total space
+          - warn:
+              when: "used/total > 60%"
+              message: /var/lib/kubelet is more than 60% full
+          - pass:
+              when: "available/total >= 90%"
+              message: /var/lib/kubelet has more than 90% available
+          - pass:
+              message: /var/lib/kubelet has sufficient disk space available
diff --git a/examples/preflight/host-memory.yaml b/examples/preflight/host-memory.yaml
new file mode 100644
index 00000000..4f883efa
--- /dev/null
+++ b/examples/preflight/host-memory.yaml
@@ -0,0 +1,19 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: memory
+spec:
+  collectors:
+    - memory:
+        collectorName: memory
+  analyzers:
+    - memory:
+        outcomes:
+          - fail:
+              when: "< 8Gi"
+              message: At least 8Gi of memory is required
+          - warn:
+              when: "< 32Gi"
+              message: At least 32Gi of memory is recommended
+          - pass:
+              message: The system has as sufficient memory
diff --git a/examples/preflight/host-port.yaml b/examples/preflight/host-port.yaml
new file mode 100644
index 00000000..33f75e7b
--- /dev/null
+++ b/examples/preflight/host-port.yaml
@@ -0,0 +1,30 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: port
+spec:
+  collectors:
+    - tcpPortStatus:
+        collectorName: k8s
+        port: 7443
+  analyzers:
+    - tcpPortStatus:
+        collectorName: k8s
+        outcomes:
+          - fail:
+              when: "connection-refused"
+              message: Connection to port 7443 was refused.
+          - fail:
+              when: "address-in-use"
+              message: Another process was already listening on port 7443.
+          - fail:
+              when: "connection-timeout"
+              message: Timed out connecting to port 7443. Check your firewall.
+          - fail:
+              when: "error"
+              message: Unexpected port status
+          - pass:
+              when: "connected"
+              message: Port 7443 is open
+          - warn:
+              message: Unexpected port status
diff --git a/examples/preflight/host-tcp-load-balancer.yaml b/examples/preflight/host-tcp-load-balancer.yaml
new file mode 100644
index 00000000..4a8dcdf2
--- /dev/null
+++ b/examples/preflight/host-tcp-load-balancer.yaml
@@ -0,0 +1,31 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: loadbalancer
+spec:
+  collectors:
+    - tcpLoadBalancer:
+        collectorName: loadbalancer
+        port: 7443
+        address: 10.128.0.29:7444
+  analyzers:
+    - tcpLoadBalancer:
+        collectorName: loadbalancer
+        outcomes:
+          - fail:
+              when: "connection-refused"
+              message: Connection to port 7443 via load balancer was refused.
+          - fail:
+              when: "address-in-use"
+              message: Another process was already listening on port 7443.
+          - fail:
+              when: "connection-timeout"
+              message: Timed out connecting to port 7443 via load balancer. Check your firewall.
+          - fail:
+              when: "error"
+              message: Unexpected port status
+          - pass:
+              when: "connected"
+              message: Successfully connected to port 7443 via load balancer
+          - warn:
+              message: Unexpected port status
diff --git a/examples/preflight/sample-host-preflight.yaml b/examples/preflight/sample-host-preflight.yaml
new file mode 100644
index 00000000..659a3533
--- /dev/null
+++ b/examples/preflight/sample-host-preflight.yaml
@@ -0,0 +1,51 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: example
+spec:
+  collectors:
+    - tcpLoadBalancer:
+        collectorName: LB1
+        address: 10.1.1.1
+        port: 6443
+        timeout: 5000ms
+    - diskUsage:
+        collectorName: ephemeral
+        path: /var/lib/kubelet
+  analyzers:
+    - cpu:
+        outcomes:
+          - fail:
+              when: "< 4"
+              message: This server has less than 4 CPU cores, and we require 8, but recommend 16
+          - warn:
+              when: "< 16"
+              message: This server has at least 4 CPU cores, but we recommend 16 or more
+          - pass:
+              message: This server has sufficient CPU cores
+    - tcpLoadBalancer:
+        collectorName: LB1
+        outcomes:
+          - fail:
+              when: "connection-timeout"
+              message: The TCP Load Balancer is not forwarding traffic to this server.
+          - fail:
+              when: "address-in-use"
+              message: The local port is not available to validate the Load Balancer configuration.
+          - pass:
+              when: "connected"
+              message: The specified TCP Load Balancer appears to be properly forwarding traffic to this server.
+    - diskUsage:
+        collectorName: ephemeral
+        outcomes:
+          - fail:
+              when: "total < 20Gi"
+              message: /var/lib/kubelet has less than 20Gi of total space
+          - fail:
+              when: "available < 10Gi"
+              message: /var/lib/kubelet has less than 10Gi of disk space available
+          - fail:
+              when: "used/total > 70%"
+              message: /var/lib/kubelet is more than 70% full
+          - pass:
+              message: /var/lib/kubelet has sufficient disk space available
diff --git a/go.mod b/go.mod
index 1a608adf..f7c21055 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/prometheus/procfs v0.0.5 // indirect
 	github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
 	github.com/segmentio/ksuid v1.0.3
+	github.com/shirou/gopsutil v3.20.12+incompatible
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.4.0
diff --git a/go.sum b/go.sum
index 05aee0bd..63763602 100644
--- a/go.sum
+++ b/go.sum
@@ -414,6 +414,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
 github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=
 github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shirou/gopsutil v3.20.12+incompatible h1:6VEGkOXP/eP4o2Ilk8cSsX0PhOEfX6leqAnD+urrp9M=
+github.com/shirou/gopsutil v3.20.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
 github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
 github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go
index 775d3fea..53d548b8 100644
--- a/pkg/analyze/analyzer.go
+++ b/pkg/analyze/analyzer.go
@@ -40,6 +40,46 @@ func isExcluded(excludeVal multitype.BoolOrString) (bool, error) {
 	return parsed, nil
 }
 
+func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	if hostAnalyzer.CPU != nil {
+		result, err := analyzeHostCPU(hostAnalyzer.CPU, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
+	if hostAnalyzer.TCPLoadBalancer != nil {
+		result, err := analyzeHostTCPLoadBalancer(hostAnalyzer.TCPLoadBalancer, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
+	if hostAnalyzer.DiskUsage != nil {
+		result, err := analyzeHostDiskUsage(hostAnalyzer.DiskUsage, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
+	if hostAnalyzer.Memory != nil {
+		result, err := analyzeHostMemory(hostAnalyzer.Memory, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
+	if hostAnalyzer.TCPPortStatus != nil {
+		result, err := analyzeHostTCPPortStatus(hostAnalyzer.TCPPortStatus, getFile)
+		if err != nil {
+			return nil, err
+		}
+		return []*AnalyzeResult{result}, nil
+	}
+
+	return nil, errors.New("invalid analyzer")
+}
+
 func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	if analyzer.ClusterVersion != nil {
 		isExcluded, err := isExcluded(analyzer.ClusterVersion.Exclude)
@@ -265,6 +305,6 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont
 		}
 		return []*AnalyzeResult{result}, nil
 	}
-	return nil, errors.New("invalid analyzer")
 
+	return nil, errors.New("invalid analyzer")
 }
diff --git a/pkg/analyze/host_cpu.go b/pkg/analyze/host_cpu.go
new file mode 100644
index 00000000..ba315cb6
--- /dev/null
+++ b/pkg/analyze/host_cpu.go
@@ -0,0 +1,165 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+)
+
+func analyzeHostCPU(hostAnalyzer *troubleshootv1beta2.CPUAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	contents, err := getCollectedFileContents("system/cpu.json")
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get collected file")
+	}
+
+	cpuInfo := collect.CPUInfo{}
+	if err := json.Unmarshal(contents, &cpuInfo); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal cpu info")
+	}
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = "Number of CPUs"
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
+			if err != nil {
+				return nil, errors.Wrap(err, "failed to compare")
+			}
+
+			if isMatch {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
+			if err != nil {
+				return nil, errors.Wrap(err, "failed to compare")
+			}
+
+			if isMatch {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
+			if err != nil {
+				return nil, errors.Wrap(err, "failed to compare")
+			}
+
+			if isMatch {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
+
+func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int) (res bool, err error) {
+	compareLogical := false
+	comparePhysical := false
+	compareUnspecified := false
+
+	comparator := ""
+	desired := ""
+
+	parts := strings.Split(conditional, " ")
+	if len(parts) == 3 {
+		comparator = parts[1]
+		desired = parts[2]
+		if strings.ToLower(parts[0]) == "logical" {
+			compareLogical = true
+		} else if strings.ToLower(parts[0]) == "physical" {
+			comparePhysical = true
+		} else if strings.ToLower(parts[0]) == "count" {
+			compareUnspecified = true
+		}
+	} else if len(parts) == 2 {
+		compareUnspecified = true
+		comparator = parts[0]
+		desired = parts[1]
+	}
+
+	if !compareLogical && !comparePhysical && !compareUnspecified {
+		return false, errors.New("unable to parse conditional")
+	}
+
+	if compareLogical {
+		return doCompareHostCPU(comparator, desired, logicalCount)
+	} else if comparePhysical {
+		return doCompareHostCPU(comparator, desired, physicalCount)
+	} else {
+		actual := logicalCount
+		if physicalCount > logicalCount {
+			actual = physicalCount
+		}
+
+		return doCompareHostCPU(comparator, desired, actual)
+	}
+}
+
+func doCompareHostCPU(operator string, desired string, actual int) (bool, error) {
+	desiredInt, err := strconv.ParseInt(desired, 10, 64)
+	if err != nil {
+		return false, errors.Wrap(err, "failed to parse")
+	}
+
+	switch operator {
+	case "<":
+		return actual < int(desiredInt), nil
+	case "<=":
+		return actual <= int(desiredInt), nil
+	case ">":
+		return actual > int(desiredInt), nil
+	case ">=":
+		return actual >= int(desiredInt), nil
+	case "=", "==", "===":
+		return actual == int(desiredInt), nil
+	}
+
+	return false, errors.New("unknown operator")
+}
diff --git a/pkg/analyze/host_disk_usage.go b/pkg/analyze/host_disk_usage.go
new file mode 100644
index 00000000..29889fdf
--- /dev/null
+++ b/pkg/analyze/host_disk_usage.go
@@ -0,0 +1,185 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+func analyzeHostDiskUsage(hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	key := collect.HostDiskUsageKey(hostAnalyzer.CollectorName)
+	contents, err := getCollectedFileContents(key)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get collected file %s", key)
+	}
+
+	diskUsageInfo := collect.DiskUsageInfo{}
+	if err := json.Unmarshal(contents, &diskUsageInfo); err != nil {
+		return nil, errors.Wrapf(err, "failed to unmarshal disk usage info from %s", key)
+	}
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = fmt.Sprintf("Disk Usage %s", hostAnalyzer.CollectorName)
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Fail.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %q", outcome.Fail.When)
+			}
+
+			if isMatch {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Warn.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %q", outcome.Warn.When)
+			}
+
+			if isMatch {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Pass.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %q", outcome.Pass.When)
+			}
+
+			if isMatch {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
+
+func compareHostDiskUsageConditionalToActual(conditional string, totalBytes uint64, usedBytes uint64) (res bool, err error) {
+	parts := strings.Split(conditional, " ")
+	if len(parts) != 3 {
+		return false, fmt.Errorf("conditional must have exactly 3 parts, got %d", len(parts))
+	}
+	stat := strings.ToLower(parts[0])
+	comparator := parts[1]
+	desired := parts[2]
+
+	switch stat {
+	case "total":
+		return doCompareHostDiskUsage(comparator, desired, totalBytes)
+	case "used":
+		return doCompareHostDiskUsage(comparator, desired, usedBytes)
+	case "available":
+		return doCompareHostDiskUsage(comparator, desired, totalBytes-usedBytes)
+	case "used/total":
+		used := float64(usedBytes) / float64(totalBytes)
+		return doCompareHostDiskUsagePercent(comparator, desired, used)
+	case "available/total":
+		available := float64(totalBytes-usedBytes) / float64(totalBytes)
+		return doCompareHostDiskUsagePercent(comparator, desired, available)
+	}
+	return false, fmt.Errorf("unknown disk usage statistic %q", stat)
+}
+
+func doCompareHostDiskUsage(operator string, desired string, actual uint64) (bool, error) {
+	quantity, err := resource.ParseQuantity(desired)
+	if err != nil {
+		return false, fmt.Errorf("could not parse quantity %q", desired)
+	}
+	desiredInt, ok := quantity.AsInt64()
+	if !ok {
+		return false, fmt.Errorf("could not parse quantity %q", desired)
+	}
+
+	switch operator {
+	case "<":
+		return actual < uint64(desiredInt), nil
+	case "<=":
+		return actual <= uint64(desiredInt), nil
+	case ">":
+		return actual > uint64(desiredInt), nil
+	case ">=":
+		return actual >= uint64(desiredInt), nil
+	case "=", "==", "===":
+		return actual == uint64(desiredInt), nil
+	}
+
+	return false, errors.New("unknown operator")
+}
+
+func doCompareHostDiskUsagePercent(operator string, desired string, actual float64) (bool, error) {
+	isPercent := false
+	if strings.HasSuffix(desired, "%") {
+		desired = strings.TrimSuffix(desired, "%")
+		isPercent = true
+	}
+	desiredPercent, err := strconv.ParseFloat(desired, 64)
+	if err != nil {
+		return false, errors.Wrap(err, "parsed desired quantity")
+	}
+	if isPercent {
+		desiredPercent = desiredPercent / 100.0
+	}
+
+	switch operator {
+	case "<":
+		return actual < desiredPercent, nil
+	case "<=":
+		return actual <= desiredPercent, nil
+	case ">":
+		return actual > desiredPercent, nil
+	case ">=":
+		return actual >= desiredPercent, nil
+	case "=", "==", "===":
+		return actual == desiredPercent, nil
+	}
+
+	return false, errors.New("unknown operator")
+}
diff --git a/pkg/analyze/host_memory.go b/pkg/analyze/host_memory.go
new file mode 100644
index 00000000..12d57b11
--- /dev/null
+++ b/pkg/analyze/host_memory.go
@@ -0,0 +1,134 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+func analyzeHostMemory(hostAnalyzer *troubleshootv1beta2.MemoryAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	contents, err := getCollectedFileContents("system/memory.json")
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get collected file")
+	}
+
+	memoryInfo := collect.MemoryInfo{}
+	if err := json.Unmarshal(contents, &memoryInfo); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal memory info")
+	}
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = "Amount of Memory"
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostMemoryConditionalToActual(outcome.Fail.When, memoryInfo.Total)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
+			}
+
+			if isMatch {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostMemoryConditionalToActual(outcome.Warn.When, memoryInfo.Total)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
+			}
+
+			if isMatch {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+
+			isMatch, err := compareHostMemoryConditionalToActual(outcome.Pass.When, memoryInfo.Total)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
+			}
+
+			if isMatch {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
+
+func compareHostMemoryConditionalToActual(conditional string, total uint64) (res bool, err error) {
+	parts := strings.Split(conditional, " ")
+	if len(parts) != 2 {
+		return false, fmt.Errorf("Expected 2 parts in conditional, got %d", len(parts))
+	}
+
+	operator := parts[0]
+	desired := parts[1]
+	quantity, err := resource.ParseQuantity(desired)
+	if err != nil {
+		return false, fmt.Errorf("could not parse quantity %q", desired)
+	}
+	desiredInt, ok := quantity.AsInt64()
+	if !ok {
+		return false, fmt.Errorf("could not parse quantity %q", desired)
+	}
+
+	switch operator {
+	case "<":
+		return total < uint64(desiredInt), nil
+	case "<=":
+		return total <= uint64(desiredInt), nil
+	case ">":
+		return total > uint64(desiredInt), nil
+	case ">=":
+		return total >= uint64(desiredInt), nil
+	case "=", "==", "===":
+		return total == uint64(desiredInt), nil
+	}
+
+	return false, errors.New("unknown operator")
+}
diff --git a/pkg/analyze/host_tcploadbalancer.go b/pkg/analyze/host_tcploadbalancer.go
new file mode 100644
index 00000000..e6efc102
--- /dev/null
+++ b/pkg/analyze/host_tcploadbalancer.go
@@ -0,0 +1,91 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+)
+
+func analyzeHostTCPLoadBalancer(hostAnalyzer *troubleshootv1beta2.TCPLoadBalancerAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	collectorName := hostAnalyzer.CollectorName
+	if collectorName == "" {
+		collectorName = "tcpLoadBalancer"
+	}
+
+	fullPath := path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", collectorName))
+
+	collected, err := getCollectedFileContents(fullPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
+	}
+	actual := collect.NetworkStatusResult{}
+	if err := json.Unmarshal(collected, &actual); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal collected")
+	}
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = "TCP Load Balancer"
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Fail.When {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Warn.When {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Pass.When {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
diff --git a/pkg/analyze/host_tcpportstatus.go b/pkg/analyze/host_tcpportstatus.go
new file mode 100644
index 00000000..4d03b1fd
--- /dev/null
+++ b/pkg/analyze/host_tcpportstatus.go
@@ -0,0 +1,89 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+)
+
+func analyzeHostTCPPortStatus(hostAnalyzer *troubleshootv1beta2.TCPPortStatusAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	fullPath := path.Join("tcpPortStatus", "tcpPortStatus.json")
+	if hostAnalyzer.CollectorName != "" {
+		fullPath = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName))
+	}
+
+	collected, err := getCollectedFileContents(fullPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
+	}
+	actual := collect.NetworkStatusResult{}
+	if err := json.Unmarshal(collected, &actual); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal collected")
+	}
+
+	result := AnalyzeResult{}
+
+	title := hostAnalyzer.CheckName
+	if title == "" {
+		title = "TCP Port Status"
+	}
+	result.Title = title
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Fail.When {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return &result, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Warn.When {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return &result, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+
+			if string(actual.Status) == outcome.Pass.When {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return &result, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
index a985823a..0b4ef14e 100644
--- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
@@ -4,18 +4,6 @@ import (
 	"github.com/replicatedhq/troubleshoot/pkg/multitype"
 )
 
-type SingleOutcome struct {
-	When    string `json:"when,omitempty" yaml:"when,omitempty"`
-	Message string `json:"message,omitempty" yaml:"message,omitempty"`
-	URI     string `json:"uri,omitempty" yaml:"uri,omitempty"`
-}
-
-type Outcome struct {
-	Fail *SingleOutcome `json:"fail,omitempty" yaml:"fail,omitempty"`
-	Warn *SingleOutcome `json:"warn,omitempty" yaml:"warn,omitempty"`
-	Pass *SingleOutcome `json:"pass,omitempty" yaml:"pass,omitempty"`
-}
-
 type ClusterVersion struct {
 	AnalyzeMeta `json:",inline" yaml:",inline"`
 	Outcomes    []*Outcome `json:"outcomes" yaml:"outcomes"`
diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
new file mode 100644
index 00000000..0900ee50
--- /dev/null
+++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
@@ -0,0 +1,41 @@
+package v1beta2
+
+type CPUAnalyze struct {
+	AnalyzeMeta `json:",inline" yaml:",inline"`
+	Outcomes    []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
+type MemoryAnalyze struct {
+	AnalyzeMeta `json:",inline" yaml:",inline"`
+	Outcomes    []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
+type TCPLoadBalancerAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
+type TCPPortStatusAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
+type DiskUsageAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
+type HostAnalyze struct {
+	CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
+	//
+	TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
+
+	DiskUsage *DiskUsageAnalyze `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
+
+	Memory *MemoryAnalyze `json:"memory,omitempty" yaml:"memory,omitempty"`
+
+	TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
new file mode 100644
index 00000000..f654a3d2
--- /dev/null
+++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
@@ -0,0 +1,80 @@
+package v1beta2
+
+import (
+	"github.com/replicatedhq/troubleshoot/pkg/multitype"
+)
+
+type HostCollectorMeta struct {
+	CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	// +optional
+	Exclude multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
+}
+
+type CPU struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+}
+
+type Memory struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+}
+
+type TCPLoadBalancer struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	Address           string `json:"address"`
+	Port              int    `json:"port"`
+	Timeout           string `json:"timeout,omitempty"`
+}
+
+type HTTPLoadBalancer struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	Address           string `json:"address"`
+	Port              int    `json:"port"`
+	Path              string `json:"path"`
+	Timeout           string `json:"timeout,omitempty"`
+}
+
+type TCPPortStatus struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	Interface         string `json:"interface,omitempty"`
+	Port              int    `json:"port"`
+}
+
+type Kubernetes struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+}
+
+type IPV4Interfaces struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+}
+
+type DiskUsage struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	Path              string `json:"path"`
+}
+
+type HostCollect struct {
+	CPU              *CPU              `json:"cpu,omitempty" yaml:"cpu,omitempty"`
+	Memory           *Memory           `json:"memory,omitempty" yaml:"memory,omitempty"`
+	TCPLoadBalancer  *TCPLoadBalancer  `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
+	HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"`
+	TCPPortStatus    *TCPPortStatus    `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
+	Kubernetes       *Kubernetes       `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
+	IPV4Interfaces   *IPV4Interfaces   `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
+	DiskUsage        *DiskUsage        `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
+}
+
+func (c *HostCollect) GetName() string {
+	var collector string
+	if c.CPU != nil {
+		collector = "cpu"
+	}
+	if c.Memory != nil {
+		collector = "memory"
+	}
+
+	if collector == "" {
+		return "<none>"
+	}
+
+	return collector
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go b/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go
new file mode 100644
index 00000000..b3834526
--- /dev/null
+++ b/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2019 Replicated, Inc..
+
+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.
+*/
+
+package v1beta2
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// HostPreflightSpec defines the desired state of HostPreflight
+type HostPreflightSpec struct {
+	Collectors []*HostCollect `json:"collectors,omitempty" yaml:"collectors,omitempty"`
+	Analyzers  []*HostAnalyze `json:"analyzers,omitempty" yaml:"analyzers,omitempty"`
+}
+
+// HostPreflightStatus defines the observed state of HostPreflight
+type HostPreflightStatus struct {
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// HostPreflight is the Schema for the hostpreflights API
+// +k8s:openapi-gen=true
+type HostPreflight struct {
+	metav1.TypeMeta   `json:",inline" yaml:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
+
+	Spec   HostPreflightSpec   `json:"spec,omitempty" yaml:"spec,omitempty"`
+	Status HostPreflightStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// HostPreflightList contains a list of HostPreflight
+type HostPreflightList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []HostPreflight `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&HostPreflight{}, &HostPreflightList{})
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/outcome.go b/pkg/apis/troubleshoot/v1beta2/outcome.go
new file mode 100644
index 00000000..4cdc572f
--- /dev/null
+++ b/pkg/apis/troubleshoot/v1beta2/outcome.go
@@ -0,0 +1,13 @@
+package v1beta2
+
+type SingleOutcome struct {
+	When    string `json:"when,omitempty" yaml:"when,omitempty"`
+	Message string `json:"message,omitempty" yaml:"message,omitempty"`
+	URI     string `json:"uri,omitempty" yaml:"uri,omitempty"`
+}
+
+type Outcome struct {
+	Fail *SingleOutcome `json:"fail,omitempty" yaml:"fail,omitempty"`
+	Warn *SingleOutcome `json:"warn,omitempty" yaml:"warn,omitempty"`
+	Pass *SingleOutcome `json:"pass,omitempty" yaml:"pass,omitempty"`
+}
diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
index 70de7ae3..dc18d28e 100644
--- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
@@ -287,6 +287,49 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CPU) DeepCopyInto(out *CPU) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPU.
+func (in *CPU) DeepCopy() *CPU {
+	if in == nil {
+		return nil
+	}
+	out := new(CPU)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CPUAnalyze) DeepCopyInto(out *CPUAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPUAnalyze.
+func (in *CPUAnalyze) DeepCopy() *CPUAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(CPUAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Ceph) DeepCopyInto(out *Ceph) {
 	*out = *in
@@ -810,6 +853,49 @@ func (in *DeploymentStatus) DeepCopy() *DeploymentStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DiskUsage) DeepCopyInto(out *DiskUsage) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiskUsage.
+func (in *DiskUsage) DeepCopy() *DiskUsage {
+	if in == nil {
+		return nil
+	}
+	out := new(DiskUsage)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DiskUsageAnalyze) DeepCopyInto(out *DiskUsageAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiskUsageAnalyze.
+func (in *DiskUsageAnalyze) DeepCopy() *DiskUsageAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(DiskUsageAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Distribution) DeepCopyInto(out *Distribution) {
 	*out = *in
@@ -941,6 +1027,260 @@ func (in *HTTP) DeepCopy() *HTTP {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPLoadBalancer) DeepCopyInto(out *HTTPLoadBalancer) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPLoadBalancer.
+func (in *HTTPLoadBalancer) DeepCopy() *HTTPLoadBalancer {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPLoadBalancer)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
+	*out = *in
+	if in.CPU != nil {
+		in, out := &in.CPU, &out.CPU
+		*out = new(CPUAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.TCPLoadBalancer != nil {
+		in, out := &in.TCPLoadBalancer, &out.TCPLoadBalancer
+		*out = new(TCPLoadBalancerAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.DiskUsage != nil {
+		in, out := &in.DiskUsage, &out.DiskUsage
+		*out = new(DiskUsageAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Memory != nil {
+		in, out := &in.Memory, &out.Memory
+		*out = new(MemoryAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.TCPPortStatus != nil {
+		in, out := &in.TCPPortStatus, &out.TCPPortStatus
+		*out = new(TCPPortStatusAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
+func (in *HostAnalyze) DeepCopy() *HostAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(HostAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostCollect) DeepCopyInto(out *HostCollect) {
+	*out = *in
+	if in.CPU != nil {
+		in, out := &in.CPU, &out.CPU
+		*out = new(CPU)
+		**out = **in
+	}
+	if in.Memory != nil {
+		in, out := &in.Memory, &out.Memory
+		*out = new(Memory)
+		**out = **in
+	}
+	if in.TCPLoadBalancer != nil {
+		in, out := &in.TCPLoadBalancer, &out.TCPLoadBalancer
+		*out = new(TCPLoadBalancer)
+		**out = **in
+	}
+	if in.HTTPLoadBalancer != nil {
+		in, out := &in.HTTPLoadBalancer, &out.HTTPLoadBalancer
+		*out = new(HTTPLoadBalancer)
+		**out = **in
+	}
+	if in.TCPPortStatus != nil {
+		in, out := &in.TCPPortStatus, &out.TCPPortStatus
+		*out = new(TCPPortStatus)
+		**out = **in
+	}
+	if in.Kubernetes != nil {
+		in, out := &in.Kubernetes, &out.Kubernetes
+		*out = new(Kubernetes)
+		**out = **in
+	}
+	if in.IPV4Interfaces != nil {
+		in, out := &in.IPV4Interfaces, &out.IPV4Interfaces
+		*out = new(IPV4Interfaces)
+		**out = **in
+	}
+	if in.DiskUsage != nil {
+		in, out := &in.DiskUsage, &out.DiskUsage
+		*out = new(DiskUsage)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
+func (in *HostCollect) DeepCopy() *HostCollect {
+	if in == nil {
+		return nil
+	}
+	out := new(HostCollect)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostCollectorMeta) DeepCopyInto(out *HostCollectorMeta) {
+	*out = *in
+	out.Exclude = in.Exclude
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollectorMeta.
+func (in *HostCollectorMeta) DeepCopy() *HostCollectorMeta {
+	if in == nil {
+		return nil
+	}
+	out := new(HostCollectorMeta)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostPreflight) DeepCopyInto(out *HostPreflight) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflight.
+func (in *HostPreflight) DeepCopy() *HostPreflight {
+	if in == nil {
+		return nil
+	}
+	out := new(HostPreflight)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HostPreflight) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostPreflightList) DeepCopyInto(out *HostPreflightList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]HostPreflight, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightList.
+func (in *HostPreflightList) DeepCopy() *HostPreflightList {
+	if in == nil {
+		return nil
+	}
+	out := new(HostPreflightList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HostPreflightList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostPreflightSpec) DeepCopyInto(out *HostPreflightSpec) {
+	*out = *in
+	if in.Collectors != nil {
+		in, out := &in.Collectors, &out.Collectors
+		*out = make([]*HostCollect, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(HostCollect)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+	if in.Analyzers != nil {
+		in, out := &in.Analyzers, &out.Analyzers
+		*out = make([]*HostAnalyze, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(HostAnalyze)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightSpec.
+func (in *HostPreflightSpec) DeepCopy() *HostPreflightSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(HostPreflightSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HostPreflightStatus) DeepCopyInto(out *HostPreflightStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightStatus.
+func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(HostPreflightStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPV4Interfaces) DeepCopyInto(out *IPV4Interfaces) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPV4Interfaces.
+func (in *IPV4Interfaces) DeepCopy() *IPV4Interfaces {
+	if in == nil {
+		return nil
+	}
+	out := new(IPV4Interfaces)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) {
 	*out = *in
@@ -1017,6 +1357,22 @@ func (in *Ingress) DeepCopy() *Ingress {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Kubernetes) DeepCopyInto(out *Kubernetes) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kubernetes.
+func (in *Kubernetes) DeepCopy() *Kubernetes {
+	if in == nil {
+		return nil
+	}
+	out := new(Kubernetes)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *LogLimits) DeepCopyInto(out *LogLimits) {
 	*out = *in
@@ -1064,6 +1420,49 @@ func (in *Logs) DeepCopy() *Logs {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Memory) DeepCopyInto(out *Memory) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memory.
+func (in *Memory) DeepCopy() *Memory {
+	if in == nil {
+		return nil
+	}
+	out := new(Memory)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MemoryAnalyze) DeepCopyInto(out *MemoryAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemoryAnalyze.
+func (in *MemoryAnalyze) DeepCopy() *MemoryAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(MemoryAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NodeResourceFilters) DeepCopyInto(out *NodeResourceFilters) {
 	*out = *in
@@ -1769,6 +2168,92 @@ func (in *SupportBundleVersionSpec) DeepCopy() *SupportBundleVersionSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPLoadBalancer) DeepCopyInto(out *TCPLoadBalancer) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPLoadBalancer.
+func (in *TCPLoadBalancer) DeepCopy() *TCPLoadBalancer {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPLoadBalancer)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPLoadBalancerAnalyze) DeepCopyInto(out *TCPLoadBalancerAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPLoadBalancerAnalyze.
+func (in *TCPLoadBalancerAnalyze) DeepCopy() *TCPLoadBalancerAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPLoadBalancerAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPPortStatus) DeepCopyInto(out *TCPPortStatus) {
+	*out = *in
+	out.HostCollectorMeta = in.HostCollectorMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPPortStatus.
+func (in *TCPPortStatus) DeepCopy() *TCPPortStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPPortStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPPortStatusAnalyze) DeepCopyInto(out *TCPPortStatusAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPPortStatusAnalyze.
+func (in *TCPPortStatusAnalyze) DeepCopy() *TCPPortStatusAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPPortStatusAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TextAnalyze) DeepCopyInto(out *TextAnalyze) {
 	*out = *in
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go
new file mode 100644
index 00000000..761fd6d8
--- /dev/null
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2019 Replicated, Inc..
+
+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.
+*/
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeHostPreflights implements HostPreflightInterface
+type FakeHostPreflights struct {
+	Fake *FakeTroubleshootV1beta2
+	ns   string
+}
+
+var hostpreflightsResource = schema.GroupVersionResource{Group: "troubleshoot.sh", Version: "v1beta2", Resource: "hostpreflights"}
+
+var hostpreflightsKind = schema.GroupVersionKind{Group: "troubleshoot.sh", Version: "v1beta2", Kind: "HostPreflight"}
+
+// Get takes name of the hostPreflight, and returns the corresponding hostPreflight object, and an error if there is any.
+func (c *FakeHostPreflights) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.HostPreflight, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(hostpreflightsResource, c.ns, name), &v1beta2.HostPreflight{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.HostPreflight), err
+}
+
+// List takes label and field selectors, and returns the list of HostPreflights that match those selectors.
+func (c *FakeHostPreflights) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.HostPreflightList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(hostpreflightsResource, hostpreflightsKind, c.ns, opts), &v1beta2.HostPreflightList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.HostPreflightList{ListMeta: obj.(*v1beta2.HostPreflightList).ListMeta}
+	for _, item := range obj.(*v1beta2.HostPreflightList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested hostPreflights.
+func (c *FakeHostPreflights) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(hostpreflightsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a hostPreflight and creates it.  Returns the server's representation of the hostPreflight, and an error, if there is any.
+func (c *FakeHostPreflights) Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (result *v1beta2.HostPreflight, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(hostpreflightsResource, c.ns, hostPreflight), &v1beta2.HostPreflight{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.HostPreflight), err
+}
+
+// Update takes the representation of a hostPreflight and updates it. Returns the server's representation of the hostPreflight, and an error, if there is any.
+func (c *FakeHostPreflights) Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(hostpreflightsResource, c.ns, hostPreflight), &v1beta2.HostPreflight{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.HostPreflight), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeHostPreflights) UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(hostpreflightsResource, "status", c.ns, hostPreflight), &v1beta2.HostPreflight{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.HostPreflight), err
+}
+
+// Delete takes name of the hostPreflight and deletes it. Returns an error if one occurs.
+func (c *FakeHostPreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(hostpreflightsResource, c.ns, name), &v1beta2.HostPreflight{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeHostPreflights) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(hostpreflightsResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.HostPreflightList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched hostPreflight.
+func (c *FakeHostPreflights) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(hostpreflightsResource, c.ns, name, pt, data, subresources...), &v1beta2.HostPreflight{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.HostPreflight), err
+}
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go
index 7e7e28da..3e11fbec 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go
@@ -35,6 +35,10 @@ func (c *FakeTroubleshootV1beta2) Collectors(namespace string) v1beta2.Collector
 	return &FakeCollectors{c, namespace}
 }
 
+func (c *FakeTroubleshootV1beta2) HostPreflights(namespace string) v1beta2.HostPreflightInterface {
+	return &FakeHostPreflights{c, namespace}
+}
+
 func (c *FakeTroubleshootV1beta2) Preflights(namespace string) v1beta2.PreflightInterface {
 	return &FakePreflights{c, namespace}
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go
index b2023742..0779c12f 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go
@@ -21,6 +21,8 @@ type AnalyzerExpansion interface{}
 
 type CollectorExpansion interface{}
 
+type HostPreflightExpansion interface{}
+
 type PreflightExpansion interface{}
 
 type RedactorExpansion interface{}
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go
new file mode 100644
index 00000000..f360ea8a
--- /dev/null
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go
@@ -0,0 +1,194 @@
+/*
+Copyright 2019 Replicated, Inc..
+
+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.
+*/
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	"context"
+	"time"
+
+	v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	scheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// HostPreflightsGetter has a method to return a HostPreflightInterface.
+// A group's client should implement this interface.
+type HostPreflightsGetter interface {
+	HostPreflights(namespace string) HostPreflightInterface
+}
+
+// HostPreflightInterface has methods to work with HostPreflight resources.
+type HostPreflightInterface interface {
+	Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (*v1beta2.HostPreflight, error)
+	Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error)
+	UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta2.HostPreflight, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta2.HostPreflightList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error)
+	HostPreflightExpansion
+}
+
+// hostPreflights implements HostPreflightInterface
+type hostPreflights struct {
+	client rest.Interface
+	ns     string
+}
+
+// newHostPreflights returns a HostPreflights
+func newHostPreflights(c *TroubleshootV1beta2Client, namespace string) *hostPreflights {
+	return &hostPreflights{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the hostPreflight, and returns the corresponding hostPreflight object, and an error if there is any.
+func (c *hostPreflights) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.HostPreflight, err error) {
+	result = &v1beta2.HostPreflight{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of HostPreflights that match those selectors.
+func (c *hostPreflights) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.HostPreflightList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta2.HostPreflightList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested hostPreflights.
+func (c *hostPreflights) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a hostPreflight and creates it.  Returns the server's representation of the hostPreflight, and an error, if there is any.
+func (c *hostPreflights) Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (result *v1beta2.HostPreflight, err error) {
+	result = &v1beta2.HostPreflight{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(hostPreflight).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a hostPreflight and updates it. Returns the server's representation of the hostPreflight, and an error, if there is any.
+func (c *hostPreflights) Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) {
+	result = &v1beta2.HostPreflight{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		Name(hostPreflight.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(hostPreflight).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *hostPreflights) UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) {
+	result = &v1beta2.HostPreflight{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		Name(hostPreflight.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(hostPreflight).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the hostPreflight and deletes it. Returns an error if one occurs.
+func (c *hostPreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *hostPreflights) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched hostPreflight.
+func (c *hostPreflights) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error) {
+	result = &v1beta2.HostPreflight{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("hostpreflights").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
index 383b93cc..9dc93fe1 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
@@ -27,6 +27,7 @@ type TroubleshootV1beta2Interface interface {
 	RESTClient() rest.Interface
 	AnalyzersGetter
 	CollectorsGetter
+	HostPreflightsGetter
 	PreflightsGetter
 	RedactorsGetter
 	SupportBundlesGetter
@@ -45,6 +46,10 @@ func (c *TroubleshootV1beta2Client) Collectors(namespace string) CollectorInterf
 	return newCollectors(c, namespace)
 }
 
+func (c *TroubleshootV1beta2Client) HostPreflights(namespace string) HostPreflightInterface {
+	return newHostPreflights(c, namespace)
+}
+
 func (c *TroubleshootV1beta2Client) Preflights(namespace string) PreflightInterface {
 	return newPreflights(c, namespace)
 }
diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go
new file mode 100644
index 00000000..c684c9ea
--- /dev/null
+++ b/pkg/collect/host_collector.go
@@ -0,0 +1,44 @@
+package collect
+
+import (
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+)
+
+type HostCollector struct {
+	Collect *troubleshootv1beta2.HostCollect
+}
+
+type HostCollectors []*HostCollector
+
+func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = errors.Errorf("recovered rom panic: %v", r)
+		}
+	}()
+
+	if c.Collect.CPU != nil {
+		result, err = HostCPU(c)
+	} else if c.Collect.Memory != nil {
+		result, err = HostMemory(c)
+	} else if c.Collect.TCPLoadBalancer != nil {
+		result, err = HostTCPLoadBalancer(c)
+	} else if c.Collect.DiskUsage != nil {
+		result, err = HostDiskUsage(c)
+	} else if c.Collect.TCPPortStatus != nil {
+		result, err = HostTCPPortStatus(c)
+	} else {
+		err = errors.New("no spec found to run")
+		return
+	}
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+func (c *HostCollector) GetDisplayName() string {
+	return c.Collect.GetName()
+}
diff --git a/pkg/collect/host_cpu.go b/pkg/collect/host_cpu.go
new file mode 100644
index 00000000..032a3ff7
--- /dev/null
+++ b/pkg/collect/host_cpu.go
@@ -0,0 +1,38 @@
+package collect
+
+import (
+	"encoding/json"
+
+	"github.com/pkg/errors"
+	"github.com/shirou/gopsutil/cpu"
+)
+
+type CPUInfo struct {
+	LogicalCount  int `json:"logicalCount"`
+	PhysicalCount int `json:"physicalCount"`
+}
+
+func HostCPU(c *HostCollector) (map[string][]byte, error) {
+	cpuInfo := CPUInfo{}
+
+	logicalCount, err := cpu.Counts(true)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to count logical cpus")
+	}
+	cpuInfo.LogicalCount = logicalCount
+
+	physicalCount, err := cpu.Counts(false)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to count physical cpus")
+	}
+	cpuInfo.PhysicalCount = physicalCount
+
+	b, err := json.Marshal(cpuInfo)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal cpu info")
+	}
+
+	return map[string][]byte{
+		"system/cpu.json": b,
+	}, nil
+}
diff --git a/pkg/collect/host_disk_usage.go b/pkg/collect/host_disk_usage.go
new file mode 100644
index 00000000..2b751e26
--- /dev/null
+++ b/pkg/collect/host_disk_usage.go
@@ -0,0 +1,43 @@
+package collect
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/pkg/errors"
+	"github.com/shirou/gopsutil/disk"
+)
+
+type DiskUsageInfo struct {
+	TotalBytes uint64 `json:"total_bytes"`
+	UsedBytes  uint64 `json:"used_bytes"`
+}
+
+func HostDiskUsage(c *HostCollector) (map[string][]byte, error) {
+	result := map[string][]byte{}
+
+	if c.Collect.DiskUsage == nil {
+		return result, nil
+	}
+
+	du, err := disk.Usage(c.Collect.DiskUsage.Path)
+	if err != nil {
+		return result, errors.Wrapf(err, "collect disk usage for %s", c.Collect.DiskUsage.Path)
+	}
+	diskSpaceInfo := DiskUsageInfo{
+		TotalBytes: du.Total,
+		UsedBytes:  du.Used,
+	}
+	b, err := json.Marshal(diskSpaceInfo)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal disk space info")
+	}
+	key := HostDiskUsageKey(c.Collect.DiskUsage.CollectorName)
+	result[key] = b
+
+	return result, nil
+}
+
+func HostDiskUsageKey(name string) string {
+	return fmt.Sprintf("diskUsage/%s.json", name)
+}
diff --git a/pkg/collect/host_memory.go b/pkg/collect/host_memory.go
new file mode 100644
index 00000000..7cf0194e
--- /dev/null
+++ b/pkg/collect/host_memory.go
@@ -0,0 +1,31 @@
+package collect
+
+import (
+	"encoding/json"
+
+	"github.com/pkg/errors"
+	"github.com/shirou/gopsutil/mem"
+)
+
+type MemoryInfo struct {
+	Total uint64 `json:"total"`
+}
+
+func HostMemory(c *HostCollector) (map[string][]byte, error) {
+	memoryInfo := MemoryInfo{}
+
+	vmstat, err := mem.VirtualMemory()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read virtual memory")
+	}
+	memoryInfo.Total = vmstat.Available
+
+	b, err := json.Marshal(memoryInfo)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal memory info")
+	}
+
+	return map[string][]byte{
+		"system/memory.json": b,
+	}, nil
+}
diff --git a/pkg/collect/host_network.go b/pkg/collect/host_network.go
new file mode 100644
index 00000000..06cabfde
--- /dev/null
+++ b/pkg/collect/host_network.go
@@ -0,0 +1,156 @@
+package collect
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/segmentio/ksuid"
+)
+
+type NetworkStatus string
+
+const (
+	NetworkStatusAddressInUse      = "address-in-use"
+	NetworkStatusConnectionRefused = "connection-refused"
+	NetworkStatusConnectionTimeout = "connection-timeout"
+	NetworkStatusConnected         = "connected"
+	NetworkStatusErrorOther        = "error"
+)
+
+type NetworkStatusResult struct {
+	Status NetworkStatus `json:"status"`
+}
+
+func checkTCPConnection(listenAddress string, dialAddress string, timeout time.Duration) (NetworkStatus, error) {
+	lstn, err := net.Listen("tcp", listenAddress)
+	if err != nil {
+		if strings.Contains(err.Error(), "address already in use") {
+			return NetworkStatusAddressInUse, nil
+		}
+
+		return NetworkStatusErrorOther, errors.Wrap(err, "failed to create listener")
+	}
+	defer lstn.Close()
+
+	// The server may receive requests from other clients and the client request may be forwarded to
+	// other servers. The client must continue to initiate new connections and send its request
+	// token until the server responds with its token.
+	requestToken := ksuid.New().Bytes()
+	responseToken := ksuid.New().Bytes()
+
+	go func() {
+		for {
+			conn, err := lstn.Accept()
+			if err != nil {
+				continue
+			}
+
+			if handleTestConnection(conn, requestToken, responseToken) {
+				return
+			}
+		}
+	}()
+
+	stopAfter := time.Now().Add(timeout)
+
+	for {
+		if time.Now().After(stopAfter) {
+			return NetworkStatusConnectionTimeout, nil
+		}
+
+		conn, err := net.DialTimeout("tcp", dialAddress, 50*time.Millisecond)
+		if err != nil {
+			if strings.Contains(err.Error(), "i/o timeout") {
+				time.Sleep(time.Millisecond * 50)
+				continue
+			}
+			if strings.Contains(err.Error(), "connection refused") {
+				return NetworkStatusConnectionRefused, nil
+			}
+			return NetworkStatusErrorOther, errors.Wrap(err, "failed to dial")
+		}
+
+		if verifyConnectionToServer(conn, requestToken, responseToken) {
+			return NetworkStatusConnected, nil
+		}
+
+		time.Sleep(time.Millisecond * 50)
+	}
+}
+
+func handleTestConnection(conn net.Conn, requestToken []byte, responseToken []byte) bool {
+	defer func() {
+		if err := conn.Close(); err != nil {
+			fmt.Println(err.Error())
+		}
+	}()
+
+	if err := conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
+		fmt.Printf("Server failed to set read deadline: %v", err)
+		return false
+	}
+
+	buf := make([]byte, 1024)
+	_, err := conn.Read(buf)
+	if err != nil {
+		fmt.Printf("Server failed to read: %v", err)
+		return false
+	}
+
+	if !bytes.Contains(buf, requestToken) {
+		return false
+	}
+
+	if err := conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
+		fmt.Printf("Server failed to set write deadline: %v", err)
+		return false
+	}
+
+	if _, err := conn.Write(responseToken); err != nil {
+		fmt.Printf("Server failed to write: %v", err)
+		return false
+	}
+
+	return true
+}
+
+func verifyConnectionToServer(conn net.Conn, requestToken []byte, responseToken []byte) bool {
+	defer func() {
+		if err := conn.Close(); err != nil {
+			fmt.Println(err.Error())
+		}
+	}()
+
+	if err := conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
+		fmt.Printf("Client failed to set write deadline: %v", err)
+		return false
+	}
+
+	_, err := conn.Write(requestToken)
+	if err != nil {
+		fmt.Printf("Client failed to write: %v", err)
+		return false
+	}
+
+	if err := conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
+		fmt.Printf("Client failed to set read deadline: %v", err)
+		return false
+	}
+
+	buf := make([]byte, 1024)
+	_, err = conn.Read(buf)
+	if err != nil {
+		fmt.Printf("Client failed to read: %v", err)
+		return false
+	}
+
+	if !bytes.Contains(buf, responseToken) {
+		return false
+	}
+
+	return true
+}
diff --git a/pkg/collect/host_tcploadbalancer.go b/pkg/collect/host_tcploadbalancer.go
new file mode 100644
index 00000000..d561620c
--- /dev/null
+++ b/pkg/collect/host_tcploadbalancer.go
@@ -0,0 +1,57 @@
+package collect
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+type ConnectionResult int
+
+const (
+	ConnectionRefused ConnectionResult = iota
+	Connected
+	ConnectionTimeout
+	ConnectionAddressInUse
+	ErrorOther
+)
+
+func HostTCPLoadBalancer(c *HostCollector) (map[string][]byte, error) {
+	listenAddress := fmt.Sprintf("0.0.0.0:%d", c.Collect.TCPLoadBalancer.Port)
+	dialAddress := c.Collect.TCPLoadBalancer.Address
+
+	timeout := 60 * time.Minute
+	if c.Collect.TCPLoadBalancer.Timeout != "" {
+		var err error
+		timeout, err = time.ParseDuration(c.Collect.TCPLoadBalancer.Timeout)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to parse durection")
+		}
+	}
+
+	networkStatus, err := checkTCPConnection(listenAddress, dialAddress, timeout)
+	if err != nil {
+		return nil, err
+	}
+
+	result := NetworkStatusResult{
+		Status: networkStatus,
+	}
+
+	b, err := json.Marshal(result)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal result")
+	}
+
+	name := path.Join("tcpLoadBalancer", "tcpLoadBalancer.json")
+	if c.Collect.TCPLoadBalancer.CollectorName != "" {
+		name = path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", c.Collect.TCPLoadBalancer.CollectorName))
+	}
+
+	return map[string][]byte{
+		name: b,
+	}, nil
+}
diff --git a/pkg/collect/host_tcpportstatus.go b/pkg/collect/host_tcpportstatus.go
new file mode 100644
index 00000000..e06bb3b7
--- /dev/null
+++ b/pkg/collect/host_tcpportstatus.go
@@ -0,0 +1,100 @@
+package collect
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"path"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+func HostTCPPortStatus(c *HostCollector) (map[string][]byte, error) {
+	dialAddress := ""
+	listenAddress := fmt.Sprintf("0.0.0.0:%d", c.Collect.TCPPortStatus.Port)
+
+	if c.Collect.TCPPortStatus.Interface != "" {
+		iface, err := net.InterfaceByName(c.Collect.TCPPortStatus.Interface)
+		if err != nil {
+			return nil, errors.Wrapf(err, "lookup interface %s", c.Collect.TCPPortStatus.Interface)
+		}
+		ip, err := getIPv4FromInterface(iface)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get ipv4 address for interface %s", c.Collect.TCPPortStatus.Interface)
+		}
+		listenAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port)
+		dialAddress = listenAddress
+	}
+
+	if dialAddress == "" {
+		ip, err := getLocalIPv4()
+		if err != nil {
+			return nil, err
+		}
+		dialAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port)
+	}
+
+	networkStatus, err := checkTCPConnection(listenAddress, dialAddress, 10*time.Second)
+	if err != nil {
+		return nil, err
+	}
+
+	result := NetworkStatusResult{
+		Status: networkStatus,
+	}
+	b, err := json.Marshal(result)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal result")
+	}
+
+	name := path.Join("tcpPortStatus", "tcpPortStatus.json")
+	if c.Collect.TCPPortStatus.CollectorName != "" {
+		name = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", c.Collect.TCPPortStatus.CollectorName))
+	}
+	return map[string][]byte{
+		name: b,
+	}, nil
+}
+
+func getIPv4FromInterface(iface *net.Interface) (net.IP, error) {
+	addrs, err := iface.Addrs()
+	if err != nil {
+		return nil, errors.Wrap(err, "list interface addresses")
+	}
+
+	for _, addr := range addrs {
+		ip, _, err := net.ParseCIDR(addr.String())
+		if err != nil {
+			return nil, errors.Wrapf(err, "parse interface address %q", addr.String())
+		}
+		ip = ip.To4()
+		if ip != nil {
+			return ip, nil
+		}
+	}
+
+	return nil, errors.New("interface does not have an ipv4 address")
+}
+
+func getLocalIPv4() (net.IP, error) {
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return nil, errors.Wrap(err, "list host network interfaces")
+	}
+
+	for _, iface := range interfaces {
+		if iface.Flags&net.FlagUp == 0 {
+			continue
+		}
+		if iface.Flags&net.FlagLoopback != 0 {
+			continue
+		}
+		ip, _ := getIPv4FromInterface(&iface)
+		if ip != nil {
+			return ip, nil
+		}
+	}
+
+	return nil, errors.New("No network interface has an IPv4 address")
+}
diff --git a/pkg/preflight/analyze.go b/pkg/preflight/analyze.go
index 1a84efb8..1157ab3b 100644
--- a/pkg/preflight/analyze.go
+++ b/pkg/preflight/analyze.go
@@ -6,12 +6,22 @@ import (
 	"strings"
 
 	analyze "github.com/replicatedhq/troubleshoot/pkg/analyze"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 )
 
 // Analyze runs the analyze phase of preflight checks
-func (c CollectResult) Analyze() []*analyze.AnalyzeResult {
+func (c ClusterCollectResult) Analyze() []*analyze.AnalyzeResult {
+	return doAnalyze(c.AllCollectedData, c.Spec.Spec.Analyzers, nil)
+}
+
+// Analyze runs the analysze phase of host preflight checks
+func (c HostCollectResult) Analyze() []*analyze.AnalyzeResult {
+	return doAnalyze(c.AllCollectedData, nil, c.Spec.Spec.Analyzers)
+}
+
+func doAnalyze(allCollectedData map[string][]byte, analyzers []*troubleshootv1beta2.Analyze, hostAnalyzers []*troubleshootv1beta2.HostAnalyze) []*analyze.AnalyzeResult {
 	getCollectedFileContents := func(fileName string) ([]byte, error) {
-		contents, ok := c.AllCollectedData[fileName]
+		contents, ok := allCollectedData[fileName]
 		if !ok {
 			return nil, fmt.Errorf("file %s was not collected", fileName)
 		}
@@ -20,13 +30,13 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult {
 	}
 	getChildCollectedFileContents := func(prefix string) (map[string][]byte, error) {
 		matching := make(map[string][]byte)
-		for k, v := range c.AllCollectedData {
+		for k, v := range allCollectedData {
 			if strings.HasPrefix(k, prefix) {
 				matching[k] = v
 			}
 		}
 
-		for k, v := range c.AllCollectedData {
+		for k, v := range allCollectedData {
 			if ok, _ := filepath.Match(prefix, k); ok {
 				matching[k] = v
 			}
@@ -36,7 +46,7 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult {
 	}
 
 	analyzeResults := []*analyze.AnalyzeResult{}
-	for _, analyzer := range c.Spec.Spec.Analyzers {
+	for _, analyzer := range analyzers {
 		analyzeResult, err := analyze.Analyze(analyzer, getCollectedFileContents, getChildCollectedFileContents)
 		if err != nil {
 			analyzeResult = []*analyze.AnalyzeResult{
@@ -53,5 +63,21 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult {
 		}
 	}
 
+	for _, hostAnalyzer := range hostAnalyzers {
+		analyzeResult, err := analyze.HostAnalyze(hostAnalyzer, getCollectedFileContents, getChildCollectedFileContents)
+		if err != nil {
+			analyzeResult = []*analyze.AnalyzeResult{
+				{
+					IsFail:  true,
+					Title:   "Analyzer Failed",
+					Message: err.Error(),
+				},
+			}
+		}
+
+		if analyzeResult != nil {
+			analyzeResults = append(analyzeResults, analyzeResult...)
+		}
+	}
 	return analyzeResults
 }
diff --git a/pkg/preflight/collect.go b/pkg/preflight/collect.go
index 74943a4c..13f4bce0 100644
--- a/pkg/preflight/collect.go
+++ b/pkg/preflight/collect.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"github.com/pkg/errors"
+	analyze "github.com/replicatedhq/troubleshoot/pkg/analyze"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	"github.com/replicatedhq/troubleshoot/pkg/collect"
 	"k8s.io/client-go/rest"
@@ -17,13 +18,73 @@ type CollectOpts struct {
 	ProgressChan           chan interface{}
 }
 
-type CollectResult struct {
+type CollectResult interface {
+	Analyze() []*analyze.AnalyzeResult
+	IsRBACAllowed() bool
+}
+
+type ClusterCollectResult struct {
 	AllCollectedData map[string][]byte
 	Collectors       collect.Collectors
-	IsRBACAllowed    bool
+	isRBACAllowed    bool
 	Spec             *troubleshootv1beta2.Preflight
 }
 
+func (cr ClusterCollectResult) IsRBACAllowed() bool {
+	return cr.isRBACAllowed
+}
+
+type HostCollectResult struct {
+	AllCollectedData map[string][]byte
+	Collectors       collect.HostCollectors
+	Spec             *troubleshootv1beta2.HostPreflight
+}
+
+func (cr HostCollectResult) IsRBACAllowed() bool {
+	return true
+}
+
+// CollectHost runs the collection phase of host preflight checks
+func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (CollectResult, error) {
+	collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0, 0)
+	collectSpecs = append(collectSpecs, p.Spec.Collectors...)
+	collectSpecs = ensureHostCollectorInList(collectSpecs, troubleshootv1beta2.HostCollect{CPU: &troubleshootv1beta2.CPU{}})
+	collectSpecs = ensureHostCollectorInList(collectSpecs, troubleshootv1beta2.HostCollect{Memory: &troubleshootv1beta2.Memory{}})
+
+	allCollectedData := make(map[string][]byte)
+
+	var collectors collect.HostCollectors
+	for _, desiredCollector := range collectSpecs {
+		collector := collect.HostCollector{
+			Collect: desiredCollector,
+		}
+		collectors = append(collectors, &collector)
+	}
+
+	collectResult := HostCollectResult{
+		Collectors: collectors,
+		Spec:       p,
+	}
+
+	for _, collector := range collectors {
+		result, err := collector.RunCollectorSync()
+		if err != nil {
+			opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v\n", collector.GetDisplayName(), err)
+			continue
+		}
+
+		if result != nil {
+			for k, v := range result {
+				allCollectedData[k] = v
+			}
+		}
+	}
+
+	collectResult.AllCollectedData = allCollectedData
+
+	return collectResult, nil
+}
+
 // Collect runs the collection phase of preflight checks
 func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, error) {
 	collectSpecs := make([]*troubleshootv1beta2.Collect, 0, 0)
@@ -44,7 +105,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult,
 		collectors = append(collectors, &collector)
 	}
 
-	collectResult := CollectResult{
+	collectResult := ClusterCollectResult{
 		Collectors: collectors,
 		Spec:       p,
 	}
@@ -62,7 +123,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult,
 	}
 
 	if foundForbidden && !opts.IgnorePermissionErrors {
-		collectResult.IsRBACAllowed = false
+		collectResult.isRBACAllowed = false
 		return collectResult, errors.New("insufficient permissions to run all collectors")
 	}
 
@@ -71,7 +132,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult,
 		if len(collector.RBACErrors) > 0 {
 			// don't skip clusterResources collector due to RBAC issues
 			if collector.Collect.ClusterResources == nil {
-				collectResult.IsRBACAllowed = false // not failing, but going to report this
+				collectResult.isRBACAllowed = false // not failing, but going to report this
 				opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName())
 				continue
 			}
@@ -106,3 +167,16 @@ func ensureCollectorInList(list []*troubleshootv1beta2.Collect, collector troubl
 
 	return append(list, &collector)
 }
+
+func ensureHostCollectorInList(list []*troubleshootv1beta2.HostCollect, collector troubleshootv1beta2.HostCollect) []*troubleshootv1beta2.HostCollect {
+	for _, inList := range list {
+		if collector.CPU != nil && inList.CPU != nil {
+			return list
+		}
+		if collector.Memory != nil && inList.Memory != nil {
+			return list
+		}
+	}
+
+	return append(list, &collector)
+}


Test patch:
diff --git a/pkg/analyze/host_cpu_test.go b/pkg/analyze/host_cpu_test.go
new file mode 100644
index 00000000..c117faaa
--- /dev/null
+++ b/pkg/analyze/host_cpu_test.go
@@ -0,0 +1,150 @@
+package analyzer
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_doCompareHostCPU(t *testing.T) {
+	tests := []struct {
+		name     string
+		operator string
+		desired  string
+		actual   int
+		expected bool
+	}{
+		{
+			name:     "< 16",
+			operator: "<",
+			desired:  "16",
+			actual:   8,
+			expected: true,
+		},
+		{
+			name:     "< 8 when actual is 8",
+			operator: "<",
+			desired:  "8",
+			actual:   8,
+			expected: false,
+		},
+		{
+			name:     "<= 8 when actual is 8",
+			operator: "<=",
+			desired:  "8",
+			actual:   8,
+			expected: true,
+		},
+		{
+			name:     "<= 8 when actual is 16",
+			operator: "<=",
+			desired:  "8",
+			actual:   16,
+			expected: false,
+		},
+		{
+			name:     "== 8 when actual is 16",
+			operator: "==",
+			desired:  "8",
+			actual:   16,
+			expected: false,
+		},
+		{
+			name:     "== 8 when actual is 8",
+			operator: "==",
+			desired:  "8",
+			actual:   8,
+			expected: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := doCompareHostCPU(test.operator, test.desired, test.actual)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+
+		})
+	}
+}
+
+func Test_compareHostCPUConditionalToActual(t *testing.T) {
+	tests := []struct {
+		name          string
+		when          string
+		logicalCount  int
+		physicalCount int
+		expected      bool
+	}{
+		{
+			name:          "physical > 4, when physical is 8",
+			when:          "physical > 4",
+			logicalCount:  0,
+			physicalCount: 8,
+			expected:      true,
+		},
+		{
+			name:          "physical > 4, when physical is 4",
+			when:          "physical > 4",
+			logicalCount:  0,
+			physicalCount: 4,
+			expected:      false,
+		},
+		{
+			name:          "physical > 4, when physical is 3, logical is 6",
+			when:          "physical > 4",
+			logicalCount:  6,
+			physicalCount: 3,
+			expected:      false,
+		},
+		{
+			name:          "logical > 4, when physical is 4, logical is 8",
+			when:          "logical > 4",
+			logicalCount:  8,
+			physicalCount: 4,
+			expected:      true,
+		},
+		{
+			name:          ">= 4, when physical is 2, logical is 4",
+			when:          ">= 4",
+			logicalCount:  4,
+			physicalCount: 2,
+			expected:      true,
+		},
+		{
+			name:          "count < 4, when physical is 2, logical is 4",
+			when:          "count < 4",
+			logicalCount:  4,
+			physicalCount: 2,
+			expected:      false,
+		},
+		{
+			name:          "count <= 4, when physical is 2, logical is 4",
+			when:          "count <= 4",
+			logicalCount:  4,
+			physicalCount: 2,
+			expected:      true,
+		},
+		{
+			name:          "== 4, physical is 4, logical is 4",
+			when:          "== 4",
+			logicalCount:  4,
+			physicalCount: 4,
+			expected:      true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/pkg/analyze/host_disk_usage_test.go b/pkg/analyze/host_disk_usage_test.go
new file mode 100644
index 00000000..7e734fac
--- /dev/null
+++ b/pkg/analyze/host_disk_usage_test.go
@@ -0,0 +1,380 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"testing"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_doCompareHostDiskUsage(t *testing.T) {
+	tests := []struct {
+		name     string
+		operator string
+		desired  string
+		actual   uint64
+		expected bool
+	}{
+		{
+			name:     ">= 1Gi, when actual is 2Gi",
+			operator: ">=",
+			desired:  "1Gi",
+			actual:   2147483648,
+			expected: true,
+		},
+		{
+			name:     "<= 1Gi, when actual is 1GB",
+			operator: "<=",
+			desired:  "1Gi",
+			actual:   1000000000,
+			expected: true,
+		},
+		{
+			name:     "< 20Gi, when actual is 15Gi",
+			operator: "<",
+			desired:  "20Gi",
+			actual:   15 * 1024 * 1024 * 1024,
+			expected: true,
+		},
+		{
+			name:     "< 20Gi, when actual is 20Gi",
+			operator: "<",
+			desired:  "20Gi",
+			actual:   20 * 1024 * 1024 * 1024,
+			expected: false,
+		},
+		{
+			name:     "> 1073741824, when actual is 1024",
+			operator: ">",
+			desired:  "1073741824",
+			actual:   1024,
+			expected: false,
+		},
+		{
+			name:     "= 4096, when actual is 4096",
+			operator: "=",
+			desired:  "4096",
+			actual:   4096,
+			expected: true,
+		},
+		{
+			name:     "= 4096, when actual is 1024",
+			operator: "=",
+			desired:  "4096",
+			actual:   1024,
+			expected: false,
+		},
+		{
+			name:     "= 4096, when actual is 5000",
+			operator: "=",
+			desired:  "4096",
+			actual:   5000,
+			expected: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := doCompareHostDiskUsage(test.operator, test.desired, test.actual)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+
+		})
+	}
+}
+
+func Test_doCompareHostDiskUsagePercent(t *testing.T) {
+	tests := []struct {
+		name     string
+		operator string
+		desired  string
+		actual   float64
+		expected bool
+	}{
+		{
+			name:     ">= .20, when actual is .30",
+			operator: ">=",
+			desired:  ".20",
+			actual:   .30,
+			expected: true,
+		},
+		{
+			name:     ">= .20, when actual is .10",
+			operator: ">=",
+			desired:  ".20",
+			actual:   .10,
+			expected: false,
+		},
+		{
+			name:     ">= .20, when actual is .20",
+			operator: ">=",
+			desired:  ".20",
+			actual:   .20,
+			expected: true,
+		},
+		{
+			name:     "> .20, when actual is .30",
+			operator: ">",
+			desired:  ".20",
+			actual:   .30,
+			expected: true,
+		},
+		{
+			name:     "> .20, when actual is .10",
+			operator: ">",
+			desired:  ".20",
+			actual:   .10,
+			expected: false,
+		},
+		{
+			name:     "> .20, when actual is .20",
+			operator: ">",
+			desired:  ".20",
+			actual:   .20,
+			expected: false,
+		},
+		{
+			name:     "<= .20, when actual is .30",
+			operator: "<=",
+			desired:  ".20",
+			actual:   .30,
+			expected: false,
+		},
+		{
+			name:     "<= .20, when actual is .10",
+			operator: "<=",
+			desired:  ".20",
+			actual:   .10,
+			expected: true,
+		},
+		{
+			name:     "<= .20, when actual is .20",
+			operator: "<=",
+			desired:  ".20",
+			actual:   .20,
+			expected: true,
+		},
+		{
+			name:     "< .20, when actual is .30",
+			operator: "<",
+			desired:  ".20",
+			actual:   .30,
+			expected: false,
+		},
+		{
+			name:     "< .20, when actual is .10",
+			operator: "<",
+			desired:  ".20",
+			actual:   .10,
+			expected: true,
+		},
+		{
+			name:     "< .20, when actual is .20",
+			operator: "<",
+			desired:  ".20",
+			actual:   .20,
+			expected: false,
+		},
+		{
+			name:     "= .20, when actual is .30",
+			operator: "=",
+			desired:  ".20",
+			actual:   .30,
+			expected: false,
+		},
+		{
+			name:     "= .20, when actual is .10",
+			operator: "=",
+			desired:  ".20",
+			actual:   .10,
+			expected: false,
+		},
+		{
+			name:     "= .20, when actual is .20",
+			operator: "=",
+			desired:  ".20",
+			actual:   .20,
+			expected: true,
+		},
+		{
+			name:     "= 20%, when actual is .20",
+			operator: "=",
+			desired:  "20%",
+			actual:   .20,
+			expected: true,
+		},
+		{
+			name:     "= 20%, when actual is 20",
+			operator: "=",
+			desired:  "20%",
+			actual:   20,
+			expected: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := doCompareHostDiskUsagePercent(test.operator, test.desired, test.actual)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func TestAnalyzeHostDiskUsage(t *testing.T) {
+	tests := []struct {
+		name          string
+		diskUsageInfo *collect.DiskUsageInfo
+		hostAnalyzer  *troubleshootv1beta2.DiskUsageAnalyze
+		result        *AnalyzeResult
+		expectErr     bool
+	}{
+		{
+			name: "Fail on insuffient total ephemeral disk space",
+			diskUsageInfo: &collect.DiskUsageInfo{
+				TotalBytes: 10 * 1024 * 1024 * 1024,
+				UsedBytes:  5 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{
+				CollectorName: "ephemeral",
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "used/total >= 80%",
+							Message: "/var/lib/kubelet is more than 80% full",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "total <= 10Gi",
+							Message: "/var/lib/kubelet requires at least 10Gi",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Disk Usage ephemeral",
+				IsFail:  true,
+				Message: "/var/lib/kubelet requires at least 10Gi",
+			},
+		},
+		{
+			name: "Fail on insuffient available ephemeral disk space percentage",
+			diskUsageInfo: &collect.DiskUsageInfo{
+				TotalBytes: 10 * 1024 * 1024 * 1024,
+				UsedBytes:  8 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{
+				CollectorName: "ephemeral",
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "total < 10Gi",
+							Message: "/var/lib/kubelet requires at least 10Gi",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "used/total >= 80%",
+							Message: "/var/lib/kubelet is more than 80% full",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Disk Usage ephemeral",
+				IsFail:  true,
+				Message: "/var/lib/kubelet is more than 80% full",
+			},
+		},
+		{
+			name: "Warn on high ephemeral disk space usage",
+			diskUsageInfo: &collect.DiskUsageInfo{
+				TotalBytes: 1024 * 1024 * 1024 * 1024,
+				UsedBytes:  100 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{
+				CollectorName: "ephemeral",
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "total < 10Gi",
+							Message: "/var/lib/kubelet requires at least 10Gi",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "used/total >= 80%",
+							Message: "/var/lib/kubelet is more than 80% full",
+						},
+					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							When:    "used >= 100Gi",
+							Message: "/var/lib/kubelet has more than 100Gi used",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Disk Usage ephemeral",
+				IsWarn:  true,
+				Message: "/var/lib/kubelet has more than 100Gi used",
+			},
+		},
+		{
+			name: "Pass on ephemeral disk space available",
+			diskUsageInfo: &collect.DiskUsageInfo{
+				TotalBytes: 12 * 1024 * 1024 * 1024,
+				UsedBytes:  1 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{
+				CollectorName: "ephemeral",
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "available > 10Gi",
+							Message: "/var/lib/kubelet has at least 10Gi available",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Disk Usage ephemeral",
+				IsPass:  true,
+				Message: "/var/lib/kubelet has at least 10Gi available",
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+			b, err := json.Marshal(test.diskUsageInfo)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			getCollectedFileContents := func(filename string) ([]byte, error) {
+				return b, nil
+			}
+
+			result, err := analyzeHostDiskUsage(test.hostAnalyzer, getCollectedFileContents)
+			if test.expectErr {
+				req.Error(err)
+			} else {
+				req.NoError(err)
+			}
+
+			assert.Equal(t, test.result, result)
+		})
+	}
+}
diff --git a/pkg/analyze/host_memory_test.go b/pkg/analyze/host_memory_test.go
new file mode 100644
index 00000000..a0b70ccf
--- /dev/null
+++ b/pkg/analyze/host_memory_test.go
@@ -0,0 +1,177 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"testing"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_doCompareHostMemory(t *testing.T) {
+	tests := []struct {
+		name        string
+		conditional string
+		actual      uint64
+		expected    bool
+	}{
+		{
+			name:        "< 16Gi when actual is 8Gi",
+			conditional: "< 16Gi",
+			actual:      8 * 1024 * 1024 * 1024,
+			expected:    true,
+		},
+		{
+			name:        "< 8Gi when actual is 8Gi",
+			conditional: "< 8Gi",
+			actual:      8 * 1024 * 1024 * 1024,
+			expected:    false,
+		},
+		{
+			name:        "<= 8Gi when actual is 8Gi",
+			conditional: "<= 8Gi",
+			actual:      8 * 1024 * 1024 * 1024,
+			expected:    true,
+		},
+		{
+			name:        "<= 8Gi when actual is 16Gi",
+			conditional: "<= 8Gi",
+			actual:      16 * 1024 * 1024 * 1024,
+			expected:    false,
+		},
+		{
+			name:        "== 8Gi when actual is 16Gi",
+			conditional: "== 8Gi",
+			actual:      16 * 1024 * 1024 * 1024,
+			expected:    false,
+		},
+		{
+			name:        "== 8Gi when actual is 8Gi",
+			conditional: "== 8Gi",
+			actual:      8 * 1024 * 1024 * 1024,
+			expected:    true,
+		},
+		{
+			name:        "== 8000000000 when actual is 8000000000",
+			conditional: "== 8000000000",
+			actual:      8 * 1000 * 1000 * 1000,
+			expected:    true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := compareHostMemoryConditionalToActual(test.conditional, test.actual)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+
+		})
+	}
+}
+
+func TestAnalyzeHostMemory(t *testing.T) {
+	tests := []struct {
+		name         string
+		memoryInfo   *collect.MemoryInfo
+		hostAnalyzer *troubleshootv1beta2.MemoryAnalyze
+		result       *AnalyzeResult
+		expectErr    bool
+	}{
+		{
+			name: "Pass on memory available",
+			memoryInfo: &collect.MemoryInfo{
+				Total: 8 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    ">= 4Gi",
+							Message: "System has at least 4Gi of memory",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Amount of Memory",
+				IsPass:  true,
+				Message: "System has at least 4Gi of memory",
+			},
+		},
+		{
+			name: "Fail on memory available",
+			memoryInfo: &collect.MemoryInfo{
+				Total: 8 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "< 16Gi",
+							Message: "System requires at least 16Gi of memory",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Amount of Memory",
+				IsFail:  true,
+				Message: "System requires at least 16Gi of memory",
+			},
+		},
+		{
+			name: "Warn on memory available",
+			memoryInfo: &collect.MemoryInfo{
+				Total: 8 * 1024 * 1024 * 1024,
+			},
+			hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "< 4Gi",
+							Message: "System requires at least 4Gi of memory",
+						},
+					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							When:    "<= 8Gi",
+							Message: "System performs best with more than 8Gi of memory",
+						},
+					},
+				},
+			},
+			result: &AnalyzeResult{
+				Title:   "Amount of Memory",
+				IsWarn:  true,
+				Message: "System performs best with more than 8Gi of memory",
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+			b, err := json.Marshal(test.memoryInfo)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			getCollectedFileContents := func(filename string) ([]byte, error) {
+				return b, nil
+			}
+
+			result, err := analyzeHostMemory(test.hostAnalyzer, getCollectedFileContents)
+			if test.expectErr {
+				req.Error(err)
+			} else {
+				req.NoError(err)
+			}
+
+			assert.Equal(t, test.result, result)
+		})
+	}
+}