<commit_before>// Copyright 2016 The Prometheus Authors
// 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 prober

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptrace"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/prometheus/client_golang/prometheus"
	pconfig "github.com/prometheus/common/config"
	"golang.org/x/net/publicsuffix"

	"github.com/prometheus/blackbox_exporter/config"
)

func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger log.Logger) bool {
	body, err := ioutil.ReadAll(reader)
	if err != nil {
		level.Error(logger).Log("msg", "Error reading HTTP body", "err", err)
		return false
	}
	for _, expression := range httpConfig.FailIfMatchesRegexp {
		re, err := regexp.Compile(expression)
		if err != nil {
			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
			return false
		}
		if re.Match(body) {
			level.Error(logger).Log("msg", "Body matched regular expression", "regexp", expression)
			return false
		}
	}
	for _, expression := range httpConfig.FailIfNotMatchesRegexp {
		re, err := regexp.Compile(expression)
		if err != nil {
			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
			return false
		}
		if !re.Match(body) {
			level.Error(logger).Log("msg", "Body did not match regular expression", "regexp", expression)
			return false
		}
	}
	return true
}

// roundTripTrace holds timings for a single HTTP roundtrip.
type roundTripTrace struct {
	tls           bool
	start         time.Time
	dnsDone       time.Time
	connectDone   time.Time
	gotConn       time.Time
	responseStart time.Time
	end           time.Time
}

// transport is a custom transport keeping traces for each HTTP roundtrip.
type transport struct {
	Transport http.RoundTripper
	logger    log.Logger
	traces    []*roundTripTrace
	current   *roundTripTrace
}

func newTransport(rt http.RoundTripper, logger log.Logger) *transport {
	return &transport{
		Transport: rt,
		logger:    logger,
		traces:    []*roundTripTrace{},
	}
}

// RoundTrip switches to a new trace, then runs embedded RoundTripper.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
	trace := &roundTripTrace{}
	if req.URL.Scheme == "https" {
		trace.tls = true
	}
	t.current = trace
	t.traces = append(t.traces, trace)
	defer func() { trace.end = time.Now() }()
	return t.Transport.RoundTrip(req)
}

func (t *transport) DNSStart(_ httptrace.DNSStartInfo) {
	t.current.start = time.Now()
}
func (t *transport) DNSDone(_ httptrace.DNSDoneInfo) {
	t.current.dnsDone = time.Now()
}
func (ts *transport) ConnectStart(_, _ string) {
	t := ts.current
	// No DNS resolution because we connected to IP directly.
	if t.dnsDone.IsZero() {
		t.start = time.Now()
		t.dnsDone = t.start
	}
}
func (t *transport) ConnectDone(net, addr string, err error) {
	t.current.connectDone = time.Now()
}
func (t *transport) GotConn(_ httptrace.GotConnInfo) {
	t.current.gotConn = time.Now()
}
func (t *transport) GotFirstResponseByte() {
	t.current.responseStart = time.Now()
}

func ProbeHTTP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) {
	var redirects int
	var (
		durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: "probe_http_duration_seconds",
			Help: "Duration of http request by phase, summed over all redirects",
		}, []string{"phase"})
		contentLengthGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_content_length",
			Help: "Length of http content response",
		})

		redirectsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_redirects",
			Help: "The number of redirects",
		})

		isSSLGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_ssl",
			Help: "Indicates if SSL was used for the final redirect",
		})

		statusCodeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_status_code",
			Help: "Response HTTP status code",
		})

		probeSSLEarliestCertExpiryGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_ssl_earliest_cert_expiry",
			Help: "Returns earliest SSL cert expiry in unixtime",
		})

		probeHTTPVersionGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_version",
			Help: "Returns the version of HTTP of the probe response",
		})

		probeFailedDueToRegex = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_failed_due_to_regex",
			Help: "Indicates if probe failed due to regex",
		})
	)

	for _, lv := range []string{"resolve", "connect", "tls", "processing", "transfer"} {
		durationGaugeVec.WithLabelValues(lv)
	}

	registry.MustRegister(durationGaugeVec)
	registry.MustRegister(contentLengthGauge)
	registry.MustRegister(redirectsGauge)
	registry.MustRegister(isSSLGauge)
	registry.MustRegister(statusCodeGauge)
	registry.MustRegister(probeHTTPVersionGauge)
	registry.MustRegister(probeFailedDueToRegex)

	httpConfig := module.HTTP

	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
		target = "http://" + target
	}

	targetURL, err := url.Parse(target)
	if err != nil {
		level.Error(logger).Log("msg", "Could not parse target URL", "err", err)
		return false
	}
	targetHost, targetPort, err := net.SplitHostPort(targetURL.Host)
	// If split fails, assuming it's a hostname without port part.
	if err != nil {
		targetHost = targetURL.Host
	}

	ip, lookupTime, err := chooseProtocol(module.HTTP.PreferredIPProtocol, targetHost, registry, logger)
	if err != nil {
		level.Error(logger).Log("msg", "Error resolving address", "err", err)
		return false
	}
	durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)

	httpClientConfig := module.HTTP.HTTPClientConfig
	if len(httpClientConfig.TLSConfig.ServerName) == 0 {
		// If there is no `server_name` in tls_config, use
		// the hostname of the target.
		httpClientConfig.TLSConfig.ServerName = targetHost
	}
	client, err := pconfig.NewHTTPClientFromConfig(&httpClientConfig)
	if err != nil {
		level.Error(logger).Log("msg", "Error generating HTTP client", "err", err)
		return false
	}

	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
	if err != nil {
		level.Error(logger).Log("msg", "Error generating cookiejar", "err", err)
		return false
	}
	client.Jar = jar

	// Inject transport that tracks trace for each redirect.
	tt := newTransport(client.Transport, logger)
	client.Transport = tt

	client.CheckRedirect = func(r *http.Request, via []*http.Request) error {
		level.Info(logger).Log("msg", "Received redirect", "url", r.URL.String())
		redirects = len(via)
		if redirects > 10 || httpConfig.NoFollowRedirects {
			level.Info(logger).Log("msg", "Not following redirect")
			return errors.New("Don't follow redirects")
		}
		return nil
	}

	if httpConfig.Method == "" {
		httpConfig.Method = "GET"
	}

	// Replace the host field in the URL with the IP we resolved.
	origHost := targetURL.Host
	if targetPort == "" {
		targetURL.Host = "[" + ip.String() + "]"
	} else {
		targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
	}

	var body io.Reader

	// If a body is configured, add it to the request.
	if httpConfig.Body != "" {
		body = strings.NewReader(httpConfig.Body)
	}

	request, err := http.NewRequest(httpConfig.Method, targetURL.String(), body)
	request.Host = origHost
	request = request.WithContext(ctx)
	if err != nil {
		level.Error(logger).Log("msg", "Error creating request", "err", err)
		return
	}

	for key, value := range httpConfig.Headers {
		if strings.Title(key) == "Host" {
			request.Host = value
			continue
		}
		request.Header.Set(key, value)
	}

	level.Info(logger).Log("msg", "Making HTTP request", "url", request.URL.String(), "host", request.Host)

	trace := &httptrace.ClientTrace{
		DNSStart:             tt.DNSStart,
		DNSDone:              tt.DNSDone,
		ConnectStart:         tt.ConnectStart,
		ConnectDone:          tt.ConnectDone,
		GotConn:              tt.GotConn,
		GotFirstResponseByte: tt.GotFirstResponseByte,
	}
	request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))

	resp, err := client.Do(request)
	// Err won't be nil if redirects were turned off. See https://github.com/golang/go/issues/3795
	if err != nil && resp == nil {
		level.Error(logger).Log("msg", "Error for HTTP request", "err", err)
	} else {
		defer func() {
			io.Copy(ioutil.Discard, resp.Body)
			resp.Body.Close()
		}()
		level.Info(logger).Log("msg", "Received HTTP response", "status_code", resp.StatusCode)
		if len(httpConfig.ValidStatusCodes) != 0 {
			for _, code := range httpConfig.ValidStatusCodes {
				if resp.StatusCode == code {
					success = true
					break
				}
			}
			if !success {
				level.Info(logger).Log("msg", "Invalid HTTP response status code", "status_code", resp.StatusCode,
					"valid_status_codes", fmt.Sprintf("%v", httpConfig.ValidStatusCodes))
			}
		} else if 200 <= resp.StatusCode && resp.StatusCode < 300 {
			success = true
		} else {
			level.Info(logger).Log("msg", "Invalid HTTP response status code, wanted 2xx", "status_code", resp.StatusCode)
		}

		if success && (len(httpConfig.FailIfMatchesRegexp) > 0 || len(httpConfig.FailIfNotMatchesRegexp) > 0) {
			success = matchRegularExpressions(resp.Body, httpConfig, logger)
			if success {
				probeFailedDueToRegex.Set(0)
			} else {
				probeFailedDueToRegex.Set(1)
			}
		}

		var httpVersionNumber float64
		httpVersionNumber, err = strconv.ParseFloat(strings.TrimPrefix(resp.Proto, "HTTP/"), 64)
		if err != nil {
			level.Error(logger).Log("msg", "Error parsing version number from HTTP version", "err", err)
		}
		probeHTTPVersionGauge.Set(httpVersionNumber)

		if len(httpConfig.ValidHTTPVersions) != 0 {
			found := false
			for _, version := range httpConfig.ValidHTTPVersions {
				if version == resp.Proto {
					found = true
					break
				}
			}
			if !found {
				level.Error(logger).Log("msg", "Invalid HTTP version number", "version", httpVersionNumber)
				success = false
			}
		}

	}

	if resp == nil {
		resp = &http.Response{}
	}
	for i, trace := range tt.traces {
		level.Info(logger).Log(
			"msg", "Response timings for roundtrip",
			"roundtrip", i,
			"start", trace.start,
			"dnsDone", trace.dnsDone,
			"connectDone", trace.connectDone,
			"gotConn", trace.gotConn,
			"responseStart", trace.responseStart,
			"end", trace.end,
		)
		// We get the duration for the first request from chooseProtocol.
		if i != 0 {
			durationGaugeVec.WithLabelValues("resolve").Add(trace.dnsDone.Sub(trace.start).Seconds())
		}
		// Continue here if we never got a connection because a request failed.
		if trace.gotConn.IsZero() {
			continue
		}
		if trace.tls {
			// dnsDone must be set if gotConn was set.
			durationGaugeVec.WithLabelValues("connect").Add(trace.connectDone.Sub(trace.dnsDone).Seconds())
			durationGaugeVec.WithLabelValues("tls").Add(trace.gotConn.Sub(trace.dnsDone).Seconds())
		} else {
			durationGaugeVec.WithLabelValues("connect").Add(trace.gotConn.Sub(trace.dnsDone).Seconds())
		}

		// Continue here if we never got a response from the server.
		if trace.responseStart.IsZero() {
			continue
		}
		durationGaugeVec.WithLabelValues("processing").Add(trace.responseStart.Sub(trace.gotConn).Seconds())
		durationGaugeVec.WithLabelValues("transfer").Add(trace.end.Sub(trace.responseStart).Seconds())
	}

	if resp.TLS != nil {
		isSSLGauge.Set(float64(1))
		registry.MustRegister(probeSSLEarliestCertExpiryGauge)
		probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix()))
		if httpConfig.FailIfSSL {
			level.Error(logger).Log("msg", "Final request was over SSL")
			success = false
		}
	} else if httpConfig.FailIfNotSSL {
		level.Error(logger).Log("msg", "Final request was not over SSL")
		success = false
	}

	statusCodeGauge.Set(float64(resp.StatusCode))
	contentLengthGauge.Set(float64(resp.ContentLength))
	redirectsGauge.Set(float64(redirects))
	return
}
<commit_msg>Account response read time for http prober (#363)<commit_after>// Copyright 2016 The Prometheus Authors
// 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 prober

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptrace"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/prometheus/client_golang/prometheus"
	pconfig "github.com/prometheus/common/config"
	"golang.org/x/net/publicsuffix"

	"github.com/prometheus/blackbox_exporter/config"
)

func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger log.Logger) bool {
	body, err := ioutil.ReadAll(reader)
	if err != nil {
		level.Error(logger).Log("msg", "Error reading HTTP body", "err", err)
		return false
	}
	for _, expression := range httpConfig.FailIfMatchesRegexp {
		re, err := regexp.Compile(expression)
		if err != nil {
			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
			return false
		}
		if re.Match(body) {
			level.Error(logger).Log("msg", "Body matched regular expression", "regexp", expression)
			return false
		}
	}
	for _, expression := range httpConfig.FailIfNotMatchesRegexp {
		re, err := regexp.Compile(expression)
		if err != nil {
			level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
			return false
		}
		if !re.Match(body) {
			level.Error(logger).Log("msg", "Body did not match regular expression", "regexp", expression)
			return false
		}
	}
	return true
}

// roundTripTrace holds timings for a single HTTP roundtrip.
type roundTripTrace struct {
	tls           bool
	start         time.Time
	dnsDone       time.Time
	connectDone   time.Time
	gotConn       time.Time
	responseStart time.Time
	end           time.Time
}

// transport is a custom transport keeping traces for each HTTP roundtrip.
type transport struct {
	Transport http.RoundTripper
	logger    log.Logger
	traces    []*roundTripTrace
	current   *roundTripTrace
}

func newTransport(rt http.RoundTripper, logger log.Logger) *transport {
	return &transport{
		Transport: rt,
		logger:    logger,
		traces:    []*roundTripTrace{},
	}
}

// RoundTrip switches to a new trace, then runs embedded RoundTripper.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
	trace := &roundTripTrace{}
	if req.URL.Scheme == "https" {
		trace.tls = true
	}
	t.current = trace
	t.traces = append(t.traces, trace)
	return t.Transport.RoundTrip(req)
}

func (t *transport) DNSStart(_ httptrace.DNSStartInfo) {
	t.current.start = time.Now()
}
func (t *transport) DNSDone(_ httptrace.DNSDoneInfo) {
	t.current.dnsDone = time.Now()
}
func (ts *transport) ConnectStart(_, _ string) {
	t := ts.current
	// No DNS resolution because we connected to IP directly.
	if t.dnsDone.IsZero() {
		t.start = time.Now()
		t.dnsDone = t.start
	}
}
func (t *transport) ConnectDone(net, addr string, err error) {
	t.current.connectDone = time.Now()
}
func (t *transport) GotConn(_ httptrace.GotConnInfo) {
	t.current.gotConn = time.Now()
}
func (t *transport) GotFirstResponseByte() {
	t.current.responseStart = time.Now()
}

func ProbeHTTP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) {
	var redirects int
	var (
		durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: "probe_http_duration_seconds",
			Help: "Duration of http request by phase, summed over all redirects",
		}, []string{"phase"})
		contentLengthGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_content_length",
			Help: "Length of http content response",
		})

		redirectsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_redirects",
			Help: "The number of redirects",
		})

		isSSLGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_ssl",
			Help: "Indicates if SSL was used for the final redirect",
		})

		statusCodeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_status_code",
			Help: "Response HTTP status code",
		})

		probeSSLEarliestCertExpiryGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_ssl_earliest_cert_expiry",
			Help: "Returns earliest SSL cert expiry in unixtime",
		})

		probeHTTPVersionGauge = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_http_version",
			Help: "Returns the version of HTTP of the probe response",
		})

		probeFailedDueToRegex = prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "probe_failed_due_to_regex",
			Help: "Indicates if probe failed due to regex",
		})
	)

	for _, lv := range []string{"resolve", "connect", "tls", "processing", "transfer"} {
		durationGaugeVec.WithLabelValues(lv)
	}

	registry.MustRegister(durationGaugeVec)
	registry.MustRegister(contentLengthGauge)
	registry.MustRegister(redirectsGauge)
	registry.MustRegister(isSSLGauge)
	registry.MustRegister(statusCodeGauge)
	registry.MustRegister(probeHTTPVersionGauge)
	registry.MustRegister(probeFailedDueToRegex)

	httpConfig := module.HTTP

	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
		target = "http://" + target
	}

	targetURL, err := url.Parse(target)
	if err != nil {
		level.Error(logger).Log("msg", "Could not parse target URL", "err", err)
		return false
	}
	targetHost, targetPort, err := net.SplitHostPort(targetURL.Host)
	// If split fails, assuming it's a hostname without port part.
	if err != nil {
		targetHost = targetURL.Host
	}

	ip, lookupTime, err := chooseProtocol(module.HTTP.PreferredIPProtocol, targetHost, registry, logger)
	if err != nil {
		level.Error(logger).Log("msg", "Error resolving address", "err", err)
		return false
	}
	durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)

	httpClientConfig := module.HTTP.HTTPClientConfig
	if len(httpClientConfig.TLSConfig.ServerName) == 0 {
		// If there is no `server_name` in tls_config, use
		// the hostname of the target.
		httpClientConfig.TLSConfig.ServerName = targetHost
	}
	client, err := pconfig.NewHTTPClientFromConfig(&httpClientConfig)
	if err != nil {
		level.Error(logger).Log("msg", "Error generating HTTP client", "err", err)
		return false
	}

	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
	if err != nil {
		level.Error(logger).Log("msg", "Error generating cookiejar", "err", err)
		return false
	}
	client.Jar = jar

	// Inject transport that tracks trace for each redirect.
	tt := newTransport(client.Transport, logger)
	client.Transport = tt

	client.CheckRedirect = func(r *http.Request, via []*http.Request) error {
		level.Info(logger).Log("msg", "Received redirect", "url", r.URL.String())
		redirects = len(via)
		if redirects > 10 || httpConfig.NoFollowRedirects {
			level.Info(logger).Log("msg", "Not following redirect")
			return errors.New("Don't follow redirects")
		}
		return nil
	}

	if httpConfig.Method == "" {
		httpConfig.Method = "GET"
	}

	// Replace the host field in the URL with the IP we resolved.
	origHost := targetURL.Host
	if targetPort == "" {
		targetURL.Host = "[" + ip.String() + "]"
	} else {
		targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
	}

	var body io.Reader

	// If a body is configured, add it to the request.
	if httpConfig.Body != "" {
		body = strings.NewReader(httpConfig.Body)
	}

	request, err := http.NewRequest(httpConfig.Method, targetURL.String(), body)
	request.Host = origHost
	request = request.WithContext(ctx)
	if err != nil {
		level.Error(logger).Log("msg", "Error creating request", "err", err)
		return
	}

	for key, value := range httpConfig.Headers {
		if strings.Title(key) == "Host" {
			request.Host = value
			continue
		}
		request.Header.Set(key, value)
	}

	level.Info(logger).Log("msg", "Making HTTP request", "url", request.URL.String(), "host", request.Host)

	trace := &httptrace.ClientTrace{
		DNSStart:             tt.DNSStart,
		DNSDone:              tt.DNSDone,
		ConnectStart:         tt.ConnectStart,
		ConnectDone:          tt.ConnectDone,
		GotConn:              tt.GotConn,
		GotFirstResponseByte: tt.GotFirstResponseByte,
	}
	request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))

	resp, err := client.Do(request)
	// Err won't be nil if redirects were turned off. See https://github.com/golang/go/issues/3795
	if err != nil && resp == nil {
		level.Error(logger).Log("msg", "Error for HTTP request", "err", err)
	} else {
		requestErrored := (err != nil)

		level.Info(logger).Log("msg", "Received HTTP response", "status_code", resp.StatusCode)
		if len(httpConfig.ValidStatusCodes) != 0 {
			for _, code := range httpConfig.ValidStatusCodes {
				if resp.StatusCode == code {
					success = true
					break
				}
			}
			if !success {
				level.Info(logger).Log("msg", "Invalid HTTP response status code", "status_code", resp.StatusCode,
					"valid_status_codes", fmt.Sprintf("%v", httpConfig.ValidStatusCodes))
			}
		} else if 200 <= resp.StatusCode && resp.StatusCode < 300 {
			success = true
		} else {
			level.Info(logger).Log("msg", "Invalid HTTP response status code, wanted 2xx", "status_code", resp.StatusCode)
		}

		if success && (len(httpConfig.FailIfMatchesRegexp) > 0 || len(httpConfig.FailIfNotMatchesRegexp) > 0) {
			success = matchRegularExpressions(resp.Body, httpConfig, logger)
			if success {
				probeFailedDueToRegex.Set(0)
			} else {
				probeFailedDueToRegex.Set(1)
			}
		}

		if resp != nil && !requestErrored {
			_, err = io.Copy(ioutil.Discard, resp.Body)
			if err != nil {
				level.Info(logger).Log("msg", "Failed to read HTTP response body", "err", err)
				success = false
			}

			resp.Body.Close()
		}

		// At this point body is fully read and we can write end time.
		tt.current.end = time.Now()

		var httpVersionNumber float64
		httpVersionNumber, err = strconv.ParseFloat(strings.TrimPrefix(resp.Proto, "HTTP/"), 64)
		if err != nil {
			level.Error(logger).Log("msg", "Error parsing version number from HTTP version", "err", err)
		}
		probeHTTPVersionGauge.Set(httpVersionNumber)

		if len(httpConfig.ValidHTTPVersions) != 0 {
			found := false
			for _, version := range httpConfig.ValidHTTPVersions {
				if version == resp.Proto {
					found = true
					break
				}
			}
			if !found {
				level.Error(logger).Log("msg", "Invalid HTTP version number", "version", httpVersionNumber)
				success = false
			}
		}

	}

	if resp == nil {
		resp = &http.Response{}
	}
	for i, trace := range tt.traces {
		level.Info(logger).Log(
			"msg", "Response timings for roundtrip",
			"roundtrip", i,
			"start", trace.start,
			"dnsDone", trace.dnsDone,
			"connectDone", trace.connectDone,
			"gotConn", trace.gotConn,
			"responseStart", trace.responseStart,
			"end", trace.end,
		)
		// We get the duration for the first request from chooseProtocol.
		if i != 0 {
			durationGaugeVec.WithLabelValues("resolve").Add(trace.dnsDone.Sub(trace.start).Seconds())
		}
		// Continue here if we never got a connection because a request failed.
		if trace.gotConn.IsZero() {
			continue
		}
		if trace.tls {
			// dnsDone must be set if gotConn was set.
			durationGaugeVec.WithLabelValues("connect").Add(trace.connectDone.Sub(trace.dnsDone).Seconds())
			durationGaugeVec.WithLabelValues("tls").Add(trace.gotConn.Sub(trace.dnsDone).Seconds())
		} else {
			durationGaugeVec.WithLabelValues("connect").Add(trace.gotConn.Sub(trace.dnsDone).Seconds())
		}

		// Continue here if we never got a response from the server.
		if trace.responseStart.IsZero() {
			continue
		}
		durationGaugeVec.WithLabelValues("processing").Add(trace.responseStart.Sub(trace.gotConn).Seconds())

		// Continue here if we never read the full response from the server.
		// Usually this means that request either failed or was redirected.
		if trace.end.IsZero() {
			continue
		}
		durationGaugeVec.WithLabelValues("transfer").Add(trace.end.Sub(trace.responseStart).Seconds())
	}

	if resp.TLS != nil {
		isSSLGauge.Set(float64(1))
		registry.MustRegister(probeSSLEarliestCertExpiryGauge)
		probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix()))
		if httpConfig.FailIfSSL {
			level.Error(logger).Log("msg", "Final request was over SSL")
			success = false
		}
	} else if httpConfig.FailIfNotSSL {
		level.Error(logger).Log("msg", "Final request was not over SSL")
		success = false
	}

	statusCodeGauge.Set(float64(resp.StatusCode))
	contentLengthGauge.Set(float64(resp.ContentLength))
	redirectsGauge.Set(float64(redirects))
	return
}
<|endoftext|>


<commit_before><commit_msg>configs: Fix typo in the deprecation warning about ignore_changes = ["*"]<commit_after><|endoftext|>


<commit_before><commit_msg>client retry logic for failover<commit_after><|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/coreos/coreos-cloudinit/datasource"
	"github.com/coreos/coreos-cloudinit/initialize"
	"github.com/coreos/coreos-cloudinit/system"
)

const version = "0.3.0"

func main() {
	var printVersion bool
	flag.BoolVar(&printVersion, "version", false, "Print the version and exit")

	var ignoreFailure bool
	flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")

	var file string
	flag.StringVar(&file, "from-file", "", "Read user-data from provided file")

	var url string
	flag.StringVar(&url, "from-url", "", "Download user-data from provided url")

	var workspace string
	flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")

	var sshKeyName string
	flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")

	flag.Parse()

	if printVersion == true {
		fmt.Printf("coreos-cloudinit version %s\n", version)
		os.Exit(0)
	}

	if file != "" && url != "" {
		fmt.Println("Provide one of --from-file or --from-url")
		os.Exit(1)
	}

	var ds datasource.Datasource
	if file != "" {
		ds = datasource.NewLocalFile(file)
	} else if url != "" {
		ds = datasource.NewMetadataService(url)
	} else {
		fmt.Println("Provide one of --from-file or --from-url")
		os.Exit(1)
	}

	log.Printf("Fetching user-data from datasource of type %q", ds.Type())
	userdataBytes, err := ds.Fetch()
	if err != nil {
		log.Printf("Failed fetching user-data from datasource: %v", err)
		if ignoreFailure {
			os.Exit(0)
		} else {
			os.Exit(1)
		}
	}

	if len(userdataBytes) == 0 {
		log.Printf("No user data to handle, exiting.")
		os.Exit(0)
	}

	env := initialize.NewEnvironment("/", workspace)

	userdata := string(userdataBytes)
	userdata = env.Apply(userdata)

	parsed, err := ParseUserData(userdata)
	if err != nil {
		log.Printf("Failed parsing user-data: %v", err)
		if ignoreFailure {
			os.Exit(0)
		} else {
			os.Exit(1)
		}
	}

	err = initialize.PrepWorkspace(env.Workspace())
	if err != nil {
		log.Fatalf("Failed preparing workspace: %v", err)
	}

	switch t := parsed.(type) {
	case initialize.CloudConfig:
		err = initialize.Apply(t, env)
	case system.Script:
		var path string
		path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
		if err == nil {
			var name string
			name, err = system.ExecuteScript(path)
			initialize.PersistUnitNameInWorkspace(name, workspace)
		}
	}

	if err != nil {
		log.Fatalf("Failed resolving user-data: %v", err)
	}
}

func ParseUserData(contents string) (interface{}, error) {
	header := strings.SplitN(contents, "\n", 2)[0]

	if strings.HasPrefix(header, "#!") {
		log.Printf("Parsing user-data as script")
		return system.Script(contents), nil

	} else if header == "#cloud-config" {
		log.Printf("Parsing user-data as cloud-config")
		cfg, err := initialize.NewCloudConfig(contents)
		if err != nil {
			log.Fatal(err.Error())
		}
		return *cfg, nil
	} else {
		return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
	}
}
<commit_msg>chore(release): Bump version to v0.3.0+git<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/coreos/coreos-cloudinit/datasource"
	"github.com/coreos/coreos-cloudinit/initialize"
	"github.com/coreos/coreos-cloudinit/system"
)

const version = "0.3.0+git"

func main() {
	var printVersion bool
	flag.BoolVar(&printVersion, "version", false, "Print the version and exit")

	var ignoreFailure bool
	flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")

	var file string
	flag.StringVar(&file, "from-file", "", "Read user-data from provided file")

	var url string
	flag.StringVar(&url, "from-url", "", "Download user-data from provided url")

	var workspace string
	flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")

	var sshKeyName string
	flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")

	flag.Parse()

	if printVersion == true {
		fmt.Printf("coreos-cloudinit version %s\n", version)
		os.Exit(0)
	}

	if file != "" && url != "" {
		fmt.Println("Provide one of --from-file or --from-url")
		os.Exit(1)
	}

	var ds datasource.Datasource
	if file != "" {
		ds = datasource.NewLocalFile(file)
	} else if url != "" {
		ds = datasource.NewMetadataService(url)
	} else {
		fmt.Println("Provide one of --from-file or --from-url")
		os.Exit(1)
	}

	log.Printf("Fetching user-data from datasource of type %q", ds.Type())
	userdataBytes, err := ds.Fetch()
	if err != nil {
		log.Printf("Failed fetching user-data from datasource: %v", err)
		if ignoreFailure {
			os.Exit(0)
		} else {
			os.Exit(1)
		}
	}

	if len(userdataBytes) == 0 {
		log.Printf("No user data to handle, exiting.")
		os.Exit(0)
	}

	env := initialize.NewEnvironment("/", workspace)

	userdata := string(userdataBytes)
	userdata = env.Apply(userdata)

	parsed, err := ParseUserData(userdata)
	if err != nil {
		log.Printf("Failed parsing user-data: %v", err)
		if ignoreFailure {
			os.Exit(0)
		} else {
			os.Exit(1)
		}
	}

	err = initialize.PrepWorkspace(env.Workspace())
	if err != nil {
		log.Fatalf("Failed preparing workspace: %v", err)
	}

	switch t := parsed.(type) {
	case initialize.CloudConfig:
		err = initialize.Apply(t, env)
	case system.Script:
		var path string
		path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
		if err == nil {
			var name string
			name, err = system.ExecuteScript(path)
			initialize.PersistUnitNameInWorkspace(name, workspace)
		}
	}

	if err != nil {
		log.Fatalf("Failed resolving user-data: %v", err)
	}
}

func ParseUserData(contents string) (interface{}, error) {
	header := strings.SplitN(contents, "\n", 2)[0]

	if strings.HasPrefix(header, "#!") {
		log.Printf("Parsing user-data as script")
		return system.Script(contents), nil

	} else if header == "#cloud-config" {
		log.Printf("Parsing user-data as cloud-config")
		cfg, err := initialize.NewCloudConfig(contents)
		if err != nil {
			log.Fatal(err.Error())
		}
		return *cfg, nil
	} else {
		return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
	}
}
<|endoftext|>


<commit_before>// Copyright 2013 The golibpcap Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !safe,!appengine

package pkt

/*
#include <netinet/in.h>
#include <netinet/tcp.h>
*/
import "C"
import (
	"reflect"
	"unsafe"
)

// The TcpHdr struct is a wrapper for the tcphdr struct in <netinet/tcp.h>.
type TcpHdr struct {
	cptr    *C.struct_tcphdr // see <net/tcp.h> struct tcphdr
	Source  uint16           // source port
	Dest    uint16           // destination port
	Seq     uint32           // sequence number
	AckSeq  uint32           // acknowledgement number
	Doff    uint8            // The length of the TCP header (data offset) in 32 bit words.
	Flags   uint16           // TCP flags per RFC 793, September, 1981
	Window  uint16           // window advertisement
	Check   uint16           // checksum
	UrgPtr  uint16           // urgent pointer
	payload unsafe.Pointer
}

// With an unsafe.Pointer to the block of C memory NewTcpHdr returns a filled in TcpHdr struct.
func NewTcpHdr(p unsafe.Pointer) (*TcpHdr, unsafe.Pointer) {
	tcpHead := &TcpHdr{
		cptr: (*C.struct_tcphdr)(p),
		// Since cgo does not provide access to bit fields in a struct
		// we index 12 octets in and then shift out the unneeded bits.
		Doff: *(*byte)(unsafe.Pointer(uintptr(p) + uintptr(12))) >> 4,
	}
	tcpHead.Source = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.source)))
	tcpHead.Dest = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.dest)))
	tcpHead.Seq = uint32(C.ntohl(C.uint32_t(tcpHead.cptr.seq)))
	tcpHead.AckSeq = uint32(C.ntohl(C.uint32_t(tcpHead.cptr.ack_seq)))
	// A this time (and there are no plans to support it) cgo does not
	// provide access to bit fields in a struct so this is what we are stuck
	// with.  We index 12 octets in and then use a bit mask.
	tcpHead.Flags = *(*uint16)(unsafe.Pointer(uintptr(p) + uintptr(12))) & uint16(0x01FF)
	tcpHead.Window = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.window)))
	tcpHead.Check = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.check)))
	tcpHead.UrgPtr = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.urg_ptr)))
	tcpHead.payload = unsafe.Pointer(uintptr(p) + uintptr(tcpHead.Doff*4))
	return tcpHead, tcpHead.payload
}

// PayloadLen returns the length of the TCP packet's payload in bytes.
func (h *TcpHdr) PayloadLen(pl uint16) uint16 {
	return pl - uint16(h.Doff*4)
}

// GetPayloadBytes returns the bytes from the packet's payload.  This is a Go
// slice backed by the C bytes.  The result is that the Go slice uses very
// little extra memory.
func (h *TcpHdr) GetPayloadBytes(pl uint16) []byte {
	l := int(h.PayloadLen(pl))
	if l <= 0 {
		return []byte{}
	}
	var b []byte
	sh := (*reflect.SliceHeader)((unsafe.Pointer(&b)))
	sh.Cap = l
	sh.Len = l
	sh.Data = uintptr(h.payload)
	return b
}
<commit_msg>Fixing an Endian error in pcap/pkt/tcp_unsafe.go.<commit_after>// Copyright 2013 The golibpcap Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !safe,!appengine

package pkt

/*
#include <netinet/in.h>
#include <netinet/tcp.h>
*/
import "C"
import (
	"reflect"
	"unsafe"
)

// The TcpHdr struct is a wrapper for the tcphdr struct in <netinet/tcp.h>.
type TcpHdr struct {
	cptr    *C.struct_tcphdr // see <net/tcp.h> struct tcphdr
	Source  uint16           // source port
	Dest    uint16           // destination port
	Seq     uint32           // sequence number
	AckSeq  uint32           // acknowledgement number
	Doff    uint8            // The length of the TCP header (data offset) in 32 bit words.
	Flags   uint16           // TCP flags per RFC 793, September, 1981
	Window  uint16           // window advertisement
	Check   uint16           // checksum
	UrgPtr  uint16           // urgent pointer
	payload unsafe.Pointer
}

// With an unsafe.Pointer to the block of C memory NewTcpHdr returns a filled in TcpHdr struct.
func NewTcpHdr(p unsafe.Pointer) (*TcpHdr, unsafe.Pointer) {
	tcpHead := &TcpHdr{
		cptr: (*C.struct_tcphdr)(p),
		// Since cgo does not provide access to bit fields in a struct
		// we index 12 octets in and then shift out the unneeded bits.
		Doff: *(*byte)(unsafe.Pointer(uintptr(p) + uintptr(12))) >> 4,
	}
	tcpHead.Source = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.source)))
	tcpHead.Dest = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.dest)))
	tcpHead.Seq = uint32(C.ntohl(C.uint32_t(tcpHead.cptr.seq)))
	tcpHead.AckSeq = uint32(C.ntohl(C.uint32_t(tcpHead.cptr.ack_seq)))
	// A this time (and there are no plans to support it) cgo does not
	// provide access to bit fields in a struct so this is what we are stuck
	// with.  We index 12 octets in and then use a bit mask.
	tcpHead.Flags = uint16(C.ntohs(C.uint16_t(
		*(*uint16)(unsafe.Pointer(uintptr(p) + uintptr(12)))))) & uint16(0x01FF)
	tcpHead.Window = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.window)))
	tcpHead.Check = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.check)))
	tcpHead.UrgPtr = uint16(C.ntohs(C.uint16_t(tcpHead.cptr.urg_ptr)))
	tcpHead.payload = unsafe.Pointer(uintptr(p) + uintptr(tcpHead.Doff*4))
	return tcpHead, tcpHead.payload
}

// PayloadLen returns the length of the TCP packet's payload in bytes.
func (h *TcpHdr) PayloadLen(pl uint16) uint16 {
	return pl - uint16(h.Doff*4)
}

// GetPayloadBytes returns the bytes from the packet's payload.  This is a Go
// slice backed by the C bytes.  The result is that the Go slice uses very
// little extra memory.
func (h *TcpHdr) GetPayloadBytes(pl uint16) []byte {
	l := int(h.PayloadLen(pl))
	if l <= 0 {
		return []byte{}
	}
	var b []byte
	sh := (*reflect.SliceHeader)((unsafe.Pointer(&b)))
	sh.Cap = l
	sh.Len = l
	sh.Data = uintptr(h.payload)
	return b
}
<|endoftext|>


<commit_before>/*
   PulseHA - HA Cluster Daemon
   Copyright (C) 2017  Andrew Zak <andrew@pulseha.com>

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package main

import (
	"encoding/json"
	"errors"
	p "github.com/Syleron/PulseHA/proto"
	"github.com/Syleron/PulseHA/src/utils"
	log "github.com/Sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
	"sync"
	"time"
)

/**
 * Memberlist struct type
 */
type Memberlist struct {
	Members []*Member
	sync.Mutex
}

/**

 */
func (m *Memberlist) Lock() {
	//_, _, no, _ := runtime.Caller(1)
	//log.Debugf("Memberlist:Lock() Lock set line: %d by %s", no, MyCaller())
	m.Mutex.Lock()
}

/**

 */
func (m *Memberlist) Unlock() {
	//_, _, no, _ := runtime.Caller(1)
	//log.Debugf("Memberlist:Unlock() Unlock set line: %d by %s", no, MyCaller())
	m.Mutex.Unlock()
}

/**
 * Add a member to the client list
 */
func (m *Memberlist) AddMember(hostname string, client *Client) {
	if !m.MemberExists(hostname) {
		log.Debug("Memberlist:MemberAdd() " + hostname + " added to memberlist")
		m.Lock()
		newMember := &Member{}
		newMember.setHostname(hostname)
		newMember.setStatus(p.MemberStatus_UNAVAILABLE)
		newMember.setClient(*client)
		m.Members = append(m.Members, newMember)
		m.Unlock()
	} else {
		log.Debug("Memberlist:MemberAdd() Member " + hostname + " already exists. Skipping.")
	}
}

/**
 * Remove a member from the client list by hostname
 */
func (m *Memberlist) MemberRemoveByName(hostname string) {
	log.Debug("Memberlist:MemberRemoveByName() " + hostname + " removed from the memberlist")
	m.Lock()
	defer m.Unlock()
	for i, member := range m.Members {
		if member.getHostname() == hostname {
			m.Members = append(m.Members[:i], m.Members[i+1:]...)
		}
	}
}

/**
 * Return Member by hostname
 */
func (m *Memberlist) GetMemberByHostname(hostname string) *Member {
	m.Lock()
	defer m.Unlock()
	if hostname == "" {
		log.Warning("Memberlist:GetMemberByHostname() Unable to get get member by hostname as hostname is empty!")
	}
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member
		}
	}
	return nil
}

/**
 * Return true/false whether a member exists or not.
 */
func (m *Memberlist) MemberExists(hostname string) bool {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return true
		}
	}
	return false
}

/**
 * Attempt to broadcast a client function to other nodes (clients) within the memberlist
 */
func (m *Memberlist) Broadcast(funcName protoFunction, data interface{}) {
	log.Debug("Memberlist:Broadcast() Broadcasting " + funcName.String())
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		// We don't want to broadcast to our self!
		hostname, err := utils.GetHostname()
		if err != nil {
			log.Error("cannot broadcast as unable to get local hostname")
			return
		}
		if member.getHostname() == hostname {
			continue
		}
		log.Debugf("Broadcast: %s to member %s", funcName.String(), member.getHostname())
		member.Connect()
		member.Send(funcName, data)
	}
}

/**
Setup process for the memberlist
*/
func (m *Memberlist) Setup() {
	// Load members into our memberlist slice
	m.LoadMembers()
	// Check to see if we are in a cluster
	if gconf.ClusterCheck() {
		// Are we the only member in the cluster?
		if gconf.ClusterTotal() == 1 {
			// We are the only member in the cluster so
			// we are assume that we are now the active appliance.
			m.PromoteMember(gconf.getLocalNode())
		} else {
			// come up passive and monitoring health checks
			localMember := m.GetMemberByHostname(gconf.getLocalNode())
			//localMember.setLastHCResponse(time.Now().Add(time.Duration(10) * time.Second))
			localMember.setLastHCResponse(time.Now())
			localMember.setStatus(p.MemberStatus_PASSIVE)
			log.Debug("Memberlist:Setup() - starting the monitor received health checks scheduler")
			go utils.Scheduler(localMember.monitorReceivedHCs, 2000*time.Millisecond)
		}
	}
}

/**
load the nodes in our config into our memberlist
*/
func (m *Memberlist) LoadMembers() {
	config := gconf.GetConfig()
	for key := range config.Nodes {
		newClient := &Client{}
		m.AddMember(key, newClient)
	}
}

/**

 */
func (m *Memberlist) Reload() {
	log.Debug("Memberlist:ReloadMembers() Reloading member nodes")
	// Do a config reload
	gconf.Reload()
	// clear local members
	m.LoadMembers()
}

/**
Get status of a specific member by hostname
*/
func (m *Memberlist) MemberGetStatus(hostname string) (p.MemberStatus_Status, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member.getStatus(), nil
		}
	}
	return p.MemberStatus_UNAVAILABLE, errors.New("unable to find member with hostname " + hostname)
}

/*
	Return the hostname of the active member
	or empty string if non are active
*/
func (m *Memberlist) getActiveMember() (string, *Member) {
	for _, member := range m.Members {
		if member.getStatus() == p.MemberStatus_ACTIVE {
			return member.getHostname(), member
		}
	}
	return "", nil
}

/**
Promote a member within the memberlist to become the active
node
*/
func (m *Memberlist) PromoteMember(hostname string) error {
	log.Debug("Memberlist:PromoteMember() Memberlist promoting " + hostname + " as active member..")
	// Inform everyone in the cluster that a specific node is now the new active
	// Demote if old active is no longer active. promote if the passive is the new active.
	// get host is it active?
	// Make sure the hostname member exists
	member := m.GetMemberByHostname(hostname)
	if member == nil {
		log.Warningf("Unknown hostname %s give in call to promoteMember", hostname)
		return errors.New("the specified host does not exist in the configured cluster")
	}
	// if unavailable check it works or do nothing?
	switch member.getStatus() {
	case p.MemberStatus_UNAVAILABLE:
		//If we are the only node and just configured we will be unavailable
		if gconf.nodeCount() > 1 {
			log.Warningf("Unable to promote member %s because it is unavailable", member.getHostname())
			return errors.New("unable to promote member as it is unavailable")
		}
	case p.MemberStatus_ACTIVE:
		log.Warningf("Unable to promote member %s as it is active", member.getHostname())
		return errors.New("unable to promote member as it is already active")
	}
	// get the current active member
	_, activeMember := m.getActiveMember()
	// handle if we do not have an active member
	if activeMember != nil {
		// Make the current Active appliance passive
		success := activeMember.makePassive()
		if !success {
			log.Warningf("Failed to make %s passive, continuing", activeMember.getHostname())
		}
		// TODO: Note: Do we need this?
		// Update our local value for the active member
		activeMember.setStatus(p.MemberStatus_PASSIVE)
	}
	// make the hostname the new active
	success := member.makeActive()
	// make new node active
	if !success {
		log.Warningf("Failed to promote %s to active. Falling back to %s", member.getHostname(), activeMember.getHostname())
		// Somethings gone wrong.. attempt to make the previous active - active again.
		success := activeMember.makeActive()
		if !success {
			log.Error("Failed to make reinstate the active node. Something is really wrong")
		}
		// Note: we don't need to update the active status as we should recieve an updated memberlist from the active
	}
	return nil
}

/**
	Function is only to be run on the active appliance
	Note: THis is not the final function name.. or not sure if this is
          where this logic will stay.. just playing around at this point.
	monitors the connections states for each member
*/
func (m *Memberlist) monitorClientConns() bool {
	// make sure we are still the active appliance
	member, err := m.getLocalMember()
	if err != nil {
		log.Debug("Memberlist:monitorClientConns() Client monitoring has stopped as it seems we are no longer in a cluster")
		return true
	}
	if member.getStatus() == p.MemberStatus_PASSIVE {
		log.Debug("Memberlist:monitorClientConns() Client monitoring has stopped as we are no longer active")
		return true
	}
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			continue
		}
		member.Connect()
		log.Debug(member.Hostname + " connection status is " + member.Connection.GetState().String())
		switch member.Connection.GetState() {
		case connectivity.Idle:
		case connectivity.Ready:
			member.setStatus(p.MemberStatus_PASSIVE)
		default:
			member.setStatus(p.MemberStatus_UNAVAILABLE)
		}
	}
	return false
}

/**
Send health checks to users who have a healthy connection
*/
func (m *Memberlist) addHealthCheckHandler() bool{
	// make sure we are still the active appliance
	member, err := m.getLocalMember()
	if err != nil {
		log.Debug("Memberlist:addHealthCheckhandler() Health check handler has stopped as it seems we are no longer in a cluster")
		return true
	}
	if member.getStatus() == p.MemberStatus_PASSIVE {
		log.Debug("Memberlist:addHealthCheckHandler() Health check handler has stopped as it seems we are no longer active")
		return true
	}
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			continue
		}
		if !member.getHCBusy() && member.getStatus() == p.MemberStatus_PASSIVE {
			memberlist := new(p.PulseHealthCheck)
			for _, member := range m.Members {
				newMember := &p.MemberlistMember{
					Hostname: member.getHostname(),
					Status:   member.getStatus(),
					Latency: member.getLatency(),
					LastReceived: member.getLastHCResponse().Format(time.RFC1123),
				}
				memberlist.Memberlist = append(memberlist.Memberlist, newMember)
			}
			go member.routineHC(memberlist)
		}
	}
	return false
}

/**
Sync local config with each member in the cluster.
*/
func (m *Memberlist) SyncConfig() error {
	log.Debug("Memberlist:SyncConfig Syncing config with peers..")
	// Return with our new updated config
	buf, err := json.Marshal(gconf.GetConfig())
	// Handle failure to marshal config
	if err != nil {
		return errors.New("unable to sync config " + err.Error())
	}
	m.Broadcast(SendConfigSync, &p.PulseConfigSync{
		Replicated: true,
		Config:     buf,
	})
	return nil
}

/**
Update the local memberlist statuses based on the proto memberlist message
*/
func (m *Memberlist) update(memberlist []*p.MemberlistMember) {
	log.Debug("Memberlist:update() Updating memberlist")
	m.Lock()
	defer m.Unlock()
	 //do not update the memberlist if we are active
	for _, member := range memberlist {
		for _, localMember := range m.Members {
			if member.GetHostname() == localMember.getHostname() {
				localMember.setStatus(member.Status)
				localMember.setLatency(member.Latency)
				// our local last received has priority
				if member.GetHostname() != gconf.getLocalNode() {
					tym, _ := time.Parse(time.RFC1123, member.LastReceived)
					localMember.setLastHCResponse(tym)
				}
				break
			}
		}
	}
}

/**
Calculate who's next to become active in the memberlist
*/
func (m *Memberlist) getNextActiveMember() (*Member, error) {
	for hostname, _ := range gconf.Nodes {
		member := m.GetMemberByHostname(hostname)
		if member == nil {
			panic("Memberlist:getNextActiveMember() Cannot get member by hostname " + hostname)
		}
		if member.getStatus() == p.MemberStatus_PASSIVE {
			log.Debug("Memberlist:getNextActiveMember() " + member.getHostname() + " is the new active appliance")
			return member, nil
		}
	}
	return &Member{}, errors.New("Memberlist:getNextActiveMember() No new active member found")
}

/**

*/
func (m *Memberlist) getLocalMember() (*Member, error) {
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			return member, nil
		}
	}
	return &Member{}, errors.New("cannot get local member. Perhaps we are no longer in a cluster")
}

/**
Reset the memberlist when we are no longer in a cluster.
 */
func (m *Memberlist) reset() {
	m.Lock()
	defer m.Unlock()
	m.Members = []*Member{}
}<commit_msg>words<commit_after>/*
   PulseHA - HA Cluster Daemon
   Copyright (C) 2017  Andrew Zak <andrew@pulseha.com>

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package main

import (
	"encoding/json"
	"errors"
	p "github.com/Syleron/PulseHA/proto"
	"github.com/Syleron/PulseHA/src/utils"
	log "github.com/Sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
	"sync"
	"time"
)

/**
 * Memberlist struct type
 */
type Memberlist struct {
	Members []*Member
	sync.Mutex
}

/**

 */
func (m *Memberlist) Lock() {
	m.Mutex.Lock()
}

/**

 */
func (m *Memberlist) Unlock() {
	m.Mutex.Unlock()
}

/**
 * Add a member to the client list
 */
func (m *Memberlist) AddMember(hostname string, client *Client) {
	if !m.MemberExists(hostname) {
		log.Debug("Memberlist:MemberAdd() " + hostname + " added to memberlist")
		m.Lock()
		newMember := &Member{}
		newMember.setHostname(hostname)
		newMember.setStatus(p.MemberStatus_UNAVAILABLE)
		newMember.setClient(*client)
		m.Members = append(m.Members, newMember)
		m.Unlock()
	} else {
		log.Debug("Memberlist:MemberAdd() Member " + hostname + " already exists. Skipping.")
	}
}

/**
 * Remove a member from the client list by hostname
 */
func (m *Memberlist) MemberRemoveByName(hostname string) {
	log.Debug("Memberlist:MemberRemoveByName() " + hostname + " removed from the memberlist")
	m.Lock()
	defer m.Unlock()
	for i, member := range m.Members {
		if member.getHostname() == hostname {
			m.Members = append(m.Members[:i], m.Members[i+1:]...)
		}
	}
}

/**
 * Return Member by hostname
 */
func (m *Memberlist) GetMemberByHostname(hostname string) *Member {
	m.Lock()
	defer m.Unlock()
	if hostname == "" {
		log.Warning("Memberlist:GetMemberByHostname() Unable to get get member by hostname as hostname is empty!")
	}
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member
		}
	}
	return nil
}

/**
 * Return true/false whether a member exists or not.
 */
func (m *Memberlist) MemberExists(hostname string) bool {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return true
		}
	}
	return false
}

/**
 * Attempt to broadcast a client function to other nodes (clients) within the memberlist
 */
func (m *Memberlist) Broadcast(funcName protoFunction, data interface{}) {
	log.Debug("Memberlist:Broadcast() Broadcasting " + funcName.String())
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		// We don't want to broadcast to our self!
		hostname, err := utils.GetHostname()
		if err != nil {
			log.Error("cannot broadcast as unable to get local hostname")
			return
		}
		if member.getHostname() == hostname {
			continue
		}
		log.Debugf("Broadcast: %s to member %s", funcName.String(), member.getHostname())
		member.Connect()
		member.Send(funcName, data)
	}
}

/**
Setup process for the memberlist
*/
func (m *Memberlist) Setup() {
	// Load members into our memberlist slice
	m.LoadMembers()
	// Check to see if we are in a cluster
	if gconf.ClusterCheck() {
		// Are we the only member in the cluster?
		if gconf.ClusterTotal() == 1 {
			// We are the only member in the cluster so
			// we are assume that we are now the active appliance.
			m.PromoteMember(gconf.getLocalNode())
		} else {
			// come up passive and monitoring health checks
			localMember := m.GetMemberByHostname(gconf.getLocalNode())
			localMember.setLastHCResponse(time.Now())
			localMember.setStatus(p.MemberStatus_PASSIVE)
			log.Debug("Memberlist:Setup() - starting the monitor received health checks scheduler")
			go utils.Scheduler(localMember.monitorReceivedHCs, 2000*time.Millisecond)
		}
	}
}

/**
load the nodes in our config into our memberlist
*/
func (m *Memberlist) LoadMembers() {
	config := gconf.GetConfig()
	for key := range config.Nodes {
		newClient := &Client{}
		m.AddMember(key, newClient)
	}
}

/**

 */
func (m *Memberlist) Reload() {
	log.Debug("Memberlist:ReloadMembers() Reloading member nodes")
	// Do a config reload
	gconf.Reload()
	// clear local members
	m.LoadMembers()
}

/**
Get status of a specific member by hostname
*/
func (m *Memberlist) MemberGetStatus(hostname string) (p.MemberStatus_Status, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member.getStatus(), nil
		}
	}
	return p.MemberStatus_UNAVAILABLE, errors.New("unable to find member with hostname " + hostname)
}

/*
	Return the hostname of the active member
	or empty string if non are active
*/
func (m *Memberlist) getActiveMember() (string, *Member) {
	for _, member := range m.Members {
		if member.getStatus() == p.MemberStatus_ACTIVE {
			return member.getHostname(), member
		}
	}
	return "", nil
}

/**
Promote a member within the memberlist to become the active
node
*/
func (m *Memberlist) PromoteMember(hostname string) error {
	log.Debug("Memberlist:PromoteMember() Memberlist promoting " + hostname + " as active member..")
	// Inform everyone in the cluster that a specific node is now the new active
	// Demote if old active is no longer active. promote if the passive is the new active.
	// get host is it active?
	// Make sure the hostname member exists
	member := m.GetMemberByHostname(hostname)
	if member == nil {
		log.Warningf("Unknown hostname %s give in call to promoteMember", hostname)
		return errors.New("the specified host does not exist in the configured cluster")
	}
	// if unavailable check it works or do nothing?
	switch member.getStatus() {
	case p.MemberStatus_UNAVAILABLE:
		//If we are the only node and just configured we will be unavailable
		if gconf.nodeCount() > 1 {
			log.Warningf("Unable to promote member %s because it is unavailable", member.getHostname())
			return errors.New("unable to promote member as it is unavailable")
		}
	case p.MemberStatus_ACTIVE:
		log.Warningf("Unable to promote member %s as it is active", member.getHostname())
		return errors.New("unable to promote member as it is already active")
	}
	// get the current active member
	_, activeMember := m.getActiveMember()
	// handle if we do not have an active member
	if activeMember != nil {
		// Make the current Active appliance passive
		success := activeMember.makePassive()
		if !success {
			log.Warningf("Failed to make %s passive, continuing", activeMember.getHostname())
		}
		// TODO: Note: Do we need this?
		// Update our local value for the active member
		activeMember.setStatus(p.MemberStatus_PASSIVE)
	}
	// make the hostname the new active
	success := member.makeActive()
	// make new node active
	if !success {
		log.Warningf("Failed to promote %s to active. Falling back to %s", member.getHostname(), activeMember.getHostname())
		// Somethings gone wrong.. attempt to make the previous active - active again.
		success := activeMember.makeActive()
		if !success {
			log.Error("Failed to make reinstate the active node. Something is really wrong")
		}
		// Note: we don't need to update the active status as we should receive an updated memberlist from the active
	}
	return nil
}

/**
	Function is only to be run on the active appliance
	Note: THis is not the final function name.. or not sure if this is
          where this logic will stay.. just playing around at this point.
	monitors the connections states for each member
*/
func (m *Memberlist) monitorClientConns() bool {
	// make sure we are still the active appliance
	member, err := m.getLocalMember()
	if err != nil {
		log.Debug("Memberlist:monitorClientConns() Client monitoring has stopped as it seems we are no longer in a cluster")
		return true
	}
	if member.getStatus() == p.MemberStatus_PASSIVE {
		log.Debug("Memberlist:monitorClientConns() Client monitoring has stopped as we are no longer active")
		return true
	}
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			continue
		}
		member.Connect()
		log.Debug(member.Hostname + " connection status is " + member.Connection.GetState().String())
		switch member.Connection.GetState() {
		case connectivity.Idle:
		case connectivity.Ready:
			member.setStatus(p.MemberStatus_PASSIVE)
		default:
			member.setStatus(p.MemberStatus_UNAVAILABLE)
		}
	}
	return false
}

/**
Send health checks to users who have a healthy connection
*/
func (m *Memberlist) addHealthCheckHandler() bool{
	// make sure we are still the active appliance
	member, err := m.getLocalMember()
	if err != nil {
		log.Debug("Memberlist:addHealthCheckhandler() Health check handler has stopped as it seems we are no longer in a cluster")
		return true
	}
	if member.getStatus() == p.MemberStatus_PASSIVE {
		log.Debug("Memberlist:addHealthCheckHandler() Health check handler has stopped as it seems we are no longer active")
		return true
	}
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			continue
		}
		if !member.getHCBusy() && member.getStatus() == p.MemberStatus_PASSIVE {
			memberlist := new(p.PulseHealthCheck)
			for _, member := range m.Members {
				newMember := &p.MemberlistMember{
					Hostname: member.getHostname(),
					Status:   member.getStatus(),
					Latency: member.getLatency(),
					LastReceived: member.getLastHCResponse().Format(time.RFC1123),
				}
				memberlist.Memberlist = append(memberlist.Memberlist, newMember)
			}
			go member.routineHC(memberlist)
		}
	}
	return false
}

/**
Sync local config with each member in the cluster.
*/
func (m *Memberlist) SyncConfig() error {
	log.Debug("Memberlist:SyncConfig Syncing config with peers..")
	// Return with our new updated config
	buf, err := json.Marshal(gconf.GetConfig())
	// Handle failure to marshal config
	if err != nil {
		return errors.New("unable to sync config " + err.Error())
	}
	m.Broadcast(SendConfigSync, &p.PulseConfigSync{
		Replicated: true,
		Config:     buf,
	})
	return nil
}

/**
Update the local memberlist statuses based on the proto memberlist message
*/
func (m *Memberlist) update(memberlist []*p.MemberlistMember) {
	log.Debug("Memberlist:update() Updating memberlist")
	m.Lock()
	defer m.Unlock()
	 //do not update the memberlist if we are active
	for _, member := range memberlist {
		for _, localMember := range m.Members {
			if member.GetHostname() == localMember.getHostname() {
				localMember.setStatus(member.Status)
				localMember.setLatency(member.Latency)
				// our local last received has priority
				if member.GetHostname() != gconf.getLocalNode() {
					tym, _ := time.Parse(time.RFC1123, member.LastReceived)
					localMember.setLastHCResponse(tym)
				}
				break
			}
		}
	}
}

/**
Calculate who's next to become active in the memberlist
*/
func (m *Memberlist) getNextActiveMember() (*Member, error) {
	for hostname, _ := range gconf.Nodes {
		member := m.GetMemberByHostname(hostname)
		if member == nil {
			panic("Memberlist:getNextActiveMember() Cannot get member by hostname " + hostname)
		}
		if member.getStatus() == p.MemberStatus_PASSIVE {
			log.Debug("Memberlist:getNextActiveMember() " + member.getHostname() + " is the new active appliance")
			return member, nil
		}
	}
	return &Member{}, errors.New("Memberlist:getNextActiveMember() No new active member found")
}

/**

*/
func (m *Memberlist) getLocalMember() (*Member, error) {
	for _, member := range m.Members {
		if member.getHostname() == gconf.getLocalNode() {
			return member, nil
		}
	}
	return &Member{}, errors.New("cannot get local member. Perhaps we are no longer in a cluster")
}

/**
Reset the memberlist when we are no longer in a cluster.
 */
func (m *Memberlist) reset() {
	m.Lock()
	defer m.Unlock()
	m.Members = []*Member{}
}<|endoftext|>


<commit_before>/*
    PulseHA - HA Cluster Daemon
    Copyright (C) 2017  Andrew Zak <andrew@pulseha.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package main

import (
	"reflect"
	"errors"
	"github.com/Syleron/PulseHA/src/utils"
	"github.com/coreos/go-log/log"
	"github.com/Syleron/PulseHA/proto"
	"sync"
)

/**
 * Memberlist struct type
 */
type Memberlist struct {
	Members []*Member
	sync.Mutex
}

/**
 * Member struct type
 */
type Member struct {
	Hostname   string
	Status proto.MemberStatus_Status
	Client Client
	sync.Mutex
}

/*
 Getters and setters for Member which allow us to make them go routine safe
 */

func (m *Member) getHostname()string {
	m.Lock()
	defer m.Unlock()
	return m.Hostname
}
func (m *Member) setHostname(hostname string){
	m.Lock()
	defer m.Unlock()
	m.Hostname = hostname
}

func (m *Member) getStatus()proto.MemberStatus_Status {
	m.Lock()
	defer m.Unlock()
	return m.Status
}

func (m *Member) setStatus(status proto.MemberStatus_Status) {
	m.Lock()
	defer m.Unlock()
	m.Status = status
}

/**
 * Add a member to the client list
 */
func (m *Memberlist) MemberAdd(hostname string, client *Client) {
	m.Lock()
	defer m.Unlock()
	newMember := &Member{
		Hostname: hostname,
		Status: proto.MemberStatus_UNAVAILABLE,
		Client: *client,
	}

	m.Members = append(m.Members, newMember)
}

/**
 * Remove a member from the client list by hostname
 */
func (m *Memberlist) MemberRemoveByName(hostname string) () {
	m.Lock()
	defer m.Unlock()
	for i, member := range m.Members {
		if member.Hostname == hostname {
			m.Members = append(m.Members[:i], m.Members[i+1:]...)
		}
	}
}

/**
 * Return Member by hostname
 */
func (m *Memberlist) GetMemberByHostname(hostname string) (*Member) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.Hostname == hostname {
			return member
		}
	}
	return nil
}

/**
 * Return true/false whether a member exists or not.
 */
func (m *Memberlist) MemberExists(hostname string) (bool) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.Hostname == hostname {
			return true
		}
	}
	return false
}

/**
 * Attempt to broadcast a client function to other nodes (clients) within the memberlist
 */
func (m *Memberlist) Broadcast(funcName string, params ... interface{}) (interface{}, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		funcList := member.Client.GetFuncBroadcastList()
		f := reflect.ValueOf(funcList[funcName])
		if len(params) != f.Type().NumIn() {
			return nil, errors.New("the number of passed parameters do not match the function")
		}
		vals := make([]reflect.Value, len(params))
		for k, param := range params {
			vals[k] = reflect.ValueOf(param)
		}
		f.Call(vals)
	}
	return nil, nil
}

/**
 * check how many are in the cluster
 * if more than one, request who's active.
 * if no one responds assume active.
 * This should probably populate the memberlist
 */
func (m *Memberlist) Setup() {
	// todo work out hwo to handle mutex in here
	// Load members into our memberlist slice
	m.LoadMembers()
	// Check to see if we are in a cluster
	if clusterCheck() {
		// Are we the only member in the cluster?
		if clusterTotal() == 1 {
			// We are the only member in the cluster so
			// we are assume that we are now the active appliance.
			m.PromoteMember(utils.GetHostname())
		} else {
			// Contact a member in the list to see who is the "active" node.
			// Iterate through the memberlist until a response is receive.
			// If no response has been made then assume we should become the active appliance.
		}
	}
}

/**
	load the nodes in our config into our memberlist
 */
func (m *Memberlist) LoadMembers() {
	config := gconf.GetConfig()
	for key := range config.Nodes {
		log.Debug("Memberlist:LoadMembers() " + key + " added to memberlist")
		newClient := &Client{}
		m.MemberAdd(key, newClient)
	}
}

/**
	Attempt to connect to all nodes within the memberlist.
 */
func (m *Memberlist) MembersConnect() {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		// Make sure we are not connecting to ourself!
		if member.Hostname != utils.GetHostname() {
			node, err := NodeGetByName(member.Hostname)
			if err != nil {
				log.Warning(member.Hostname + " could not be found.")
			}
			err = member.Client.Connect(node.IP, node.Port, member.Hostname)
			if err != nil {
				continue
			}
			member.Status = proto.MemberStatus_PASSIVE
		}
	}
}

/**
	Get status of a specific member by hostname
 */
func (m *Memberlist) MemberGetStatus(hostname string) (proto.MemberStatus_Status, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.Hostname == hostname {
			return member.Status, nil
		}
	}
	return proto.MemberStatus_UNAVAILABLE, errors.New("unable to find member with hostname " + hostname)
}

/**
	Promote a member within the memberlist to become the active
	node
 */
func (m *Memberlist) PromoteMember(hostname string)error {
	m.Lock()
	defer m.Unlock()
	// Inform everyone in the cluster that a specific node is now the new active
	// Demote if old active is no longer active. promote if the passive is the new active.

	// get host is it active?
	node := m.GetMemberByHostname(hostname)
	if node == nil {
		log.Errorf("Unknown hostname % give in call to promoteMember",hostname)
		return errors.New("Unknown hostname")
	}
	// if unavailable check it works or do nothing?

	// make current active node passive
	// make new node active
	// update all members

	return nil
}

/**
	Sync local config with each member in the cluster.
 */
func (m *Memberlist) SyncConfig() {
	//config := gconf.GetConfig()
}

<commit_msg>Embedding Client into member and using getters and setters for Member<commit_after>/*
    PulseHA - HA Cluster Daemon
    Copyright (C) 2017  Andrew Zak <andrew@pulseha.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package main

import (
	"reflect"
	"errors"
	"github.com/Syleron/PulseHA/src/utils"
	"github.com/coreos/go-log/log"
	"github.com/Syleron/PulseHA/proto"
	"sync"
)

/**
 * Memberlist struct type
 */
type Memberlist struct {
	Members []*Member
	sync.Mutex
}

/**
 * Member struct type
 */
type Member struct {
	hostname   string
	status proto.MemberStatus_Status
	Client
	sync.Mutex
}

/*
 Getters and setters for Member which allow us to make them go routine safe
 */

func (m *Member) getHostname()string {
	m.Lock()
	defer m.Unlock()
	return m.hostname
}
func (m *Member) setHostname(hostname string){
	m.Lock()
	defer m.Unlock()
	m.hostname = hostname
}

func (m *Member) getStatus()proto.MemberStatus_Status {
	m.Lock()
	defer m.Unlock()
	return m.status
}

func (m *Member) setStatus(status proto.MemberStatus_Status) {
	m.Lock()
	defer m.Unlock()
	m.status = status
}
func (m *Member) setClient(client Client) {
	m.Client = client
}
/**
 * Add a member to the client list
 */
func (m *Memberlist) MemberAdd(hostname string, client *Client) {
	m.Lock()
	defer m.Unlock()
	newMember := &Member{}

	newMember.setHostname(hostname)
	newMember.setStatus(proto.MemberStatus_UNAVAILABLE)
	newMember.setClient(*client)

	m.Members = append(m.Members, newMember)
}

/**
 * Remove a member from the client list by hostname
 */
func (m *Memberlist) MemberRemoveByName(hostname string) () {
	m.Lock()
	defer m.Unlock()
	for i, member := range m.Members {
		if member.getHostname() == hostname {
			m.Members = append(m.Members[:i], m.Members[i+1:]...)
		}
	}
}

/**
 * Return Member by hostname
 */
func (m *Memberlist) GetMemberByHostname(hostname string) (*Member) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member
		}
	}
	return nil
}

/**
 * Return true/false whether a member exists or not.
 */
func (m *Memberlist) MemberExists(hostname string) (bool) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return true
		}
	}
	return false
}

/**
 * Attempt to broadcast a client function to other nodes (clients) within the memberlist
 */
func (m *Memberlist) Broadcast(funcName string, params ... interface{}) (interface{}, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		funcList := member.GetFuncBroadcastList()
		f := reflect.ValueOf(funcList[funcName])
		if len(params) != f.Type().NumIn() {
			return nil, errors.New("the number of passed parameters do not match the function")
		}
		vals := make([]reflect.Value, len(params))
		for k, param := range params {
			vals[k] = reflect.ValueOf(param)
		}
		f.Call(vals)
	}
	return nil, nil
}

/**
 * check how many are in the cluster
 * if more than one, request who's active.
 * if no one responds assume active.
 * This should probably populate the memberlist
 */
func (m *Memberlist) Setup() {
	// todo work out hwo to handle mutex in here
	// Load members into our memberlist slice
	m.LoadMembers()
	// Check to see if we are in a cluster
	if clusterCheck() {
		// Are we the only member in the cluster?
		if clusterTotal() == 1 {
			// We are the only member in the cluster so
			// we are assume that we are now the active appliance.
			m.PromoteMember(utils.GetHostname())
		} else {
			// Contact a member in the list to see who is the "active" node.
			// Iterate through the memberlist until a response is receive.
			// If no response has been made then assume we should become the active appliance.
		}
	}
}

/**
	load the nodes in our config into our memberlist
 */
func (m *Memberlist) LoadMembers() {
	config := gconf.GetConfig()
	for key := range config.Nodes {
		log.Debug("Memberlist:LoadMembers() " + key + " added to memberlist")
		newClient := &Client{}
		m.MemberAdd(key, newClient)
	}
}

/**
	Attempt to connect to all nodes within the memberlist.
 */
func (m *Memberlist) MembersConnect() {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		// Make sure we are not connecting to ourself!
		if member.getHostname() != utils.GetHostname() {
			node, err := NodeGetByName(member.getHostname())
			if err != nil {
				log.Warning(member.getHostname() + " could not be found.")
			}
			err = member.Client.Connect(node.IP, node.Port, member.getHostname())
			if err != nil {
				continue
			}
			member.setStatus(proto.MemberStatus_PASSIVE)
		}
	}
}

/**
	Get status of a specific member by hostname
 */
func (m *Memberlist) MemberGetStatus(hostname string) (proto.MemberStatus_Status, error) {
	m.Lock()
	defer m.Unlock()
	for _, member := range m.Members {
		if member.getHostname() == hostname {
			return member.getStatus(), nil
		}
	}
	return proto.MemberStatus_UNAVAILABLE, errors.New("unable to find member with hostname " + hostname)
}

/**
	Promote a member within the memberlist to become the active
	node
 */
func (m *Memberlist) PromoteMember(hostname string)error {
	m.Lock()
	defer m.Unlock()
	// Inform everyone in the cluster that a specific node is now the new active
	// Demote if old active is no longer active. promote if the passive is the new active.

	// get host is it active?
	node := m.GetMemberByHostname(hostname)
	if node == nil {
		log.Errorf("Unknown hostname % give in call to promoteMember",hostname)
		return errors.New("Unknown hostname")
	}
	// if unavailable check it works or do nothing?

	// make current active node passive
	// make new node active
	// update all members

	return nil
}

/**
	Sync local config with each member in the cluster.
 */
func (m *Memberlist) SyncConfig() {
	//config := gconf.GetConfig()
}

<|endoftext|>


<commit_before>package daemon

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"path"
	"sync"
	"syscall"

	"github.com/subgraph/oz"
	"github.com/subgraph/oz/fs"
	"github.com/subgraph/oz/xpra"
	"github.com/subgraph/oz/network"

	"github.com/op/go-logging"
)

const initPath = "/usr/local/bin/oz-init"

type Sandbox struct {
	daemon  *daemonState
	id      int
	display int
	profile *oz.Profile
	init    *exec.Cmd
	cred    *syscall.Credential
	fs      *fs.Filesystem
	stderr  io.ReadCloser
	addr    string
	xpra    *xpra.Xpra
	ready   sync.WaitGroup
	network *network.SandboxNetwork
}

/*
func findSandbox(id int) *Sandbox {
	for _, sb := range sandboxes {
		if sb.id == id {
			return sb
		}
	}
	return nil
}
*/

const initCloneFlags = syscall.CLONE_NEWNS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWNET

func createInitCommand(name, chroot string, uid uint32, display int, stn *network.SandboxNetwork) *exec.Cmd {
	cmd := exec.Command(initPath)
	cmd.Dir = "/"
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Chroot:     chroot,
		Cloneflags: initCloneFlags,
	}
	cmd.Env = []string{
		"INIT_PROFILE=" + name,
		fmt.Sprintf("INIT_UID=%d", uid),
	}

	if stn.Ip != "" {
		cmd.Env = append(cmd.Env, "INIT_ADDR=" + stn.Ip)
		cmd.Env = append(cmd.Env, "INIT_VHOST=" + stn.VethHost)
		cmd.Env = append(cmd.Env, "INIT_VGUEST=" + stn.VethGuest)
		cmd.Env = append(cmd.Env, "INIT_GATEWAY=" + stn.Gateway.String() + "/" + stn.Class)
	}

	if display > 0 {
		cmd.Env = append(cmd.Env, fmt.Sprintf("INIT_DISPLAY=%d", display))
	}

	return cmd
}

func (d *daemonState) launch(p *oz.Profile, uid, gid uint32, log *logging.Logger) (*Sandbox, error) {
	u, err := user.LookupId(fmt.Sprintf("%d", uid))
	if err != nil {
		return nil, fmt.Errorf("failed to lookup user for uid=%d: %v", uid, err)
	}
	fs := fs.NewFromProfile(p, u, d.config.SandboxPath, d.log)
	if err := fs.Setup(d.config.ProfileDir); err != nil {
		return nil, err
	}
	display := 0
	if p.XServer.Enabled  && p.Networking.Nettype == "host" {
		display = d.nextDisplay
		d.nextDisplay += 1
	}

	stn := new(network.SandboxNetwork)
	if p.Networking.Nettype == "bridge" {
		stn, err = network.PrepareSandboxNetwork(d.network, log)
		if err != nil {
			return nil, fmt.Errorf("Unable to prepare veth network: %+v", err)
		}
	}

	cmd := createInitCommand(p.Name, fs.Root(), uid, display, stn)
	log.Debug("Command environment: %+v", cmd.Env)
	pp, err := cmd.StderrPipe()
	if err != nil {
		fs.Cleanup()
		return nil, fmt.Errorf("error creating stderr pipe for init process: %v", err)

	}

	if err := cmd.Start(); err != nil {
		fs.Cleanup()
		return nil, fmt.Errorf("Unable to start process: %+v", err)
	}

	sbox := &Sandbox{
		daemon:  d,
		id:      d.nextSboxId,
		display: display,
		profile: p,
		init:    cmd,
		cred:    &syscall.Credential{Uid: uid, Gid: gid},
		fs:      fs,
		addr:    path.Join(fs.Root(), "tmp", "oz-init-control"),
		stderr:  pp,
		network: stn,
	}

	if p.Networking.Nettype == "bridge" {
		if err := network.NetInit(stn, d.network, cmd.Process.Pid, log); err != nil {
			cmd.Process.Kill()
			fs.Cleanup()
			return nil, fmt.Errorf("Unable to create veth networking: %+v", err)
		}
	}

	sbox.ready.Add(1)
	go sbox.logMessages()
	if sbox.profile.XServer.Enabled {
		go func() {
			sbox.ready.Wait()
			go sbox.startXpraClient()
		}()
	}
	d.nextSboxId += 1
	d.sandboxes = append(d.sandboxes, sbox)
	return sbox, nil
}

func (sbox *Sandbox) remove(log *logging.Logger) {
	sboxes := []*Sandbox{}
	for _, sb := range sbox.daemon.sandboxes {
		if sb == sbox {
			sb.fs.Cleanup()
			if sb.profile.Networking.Nettype == "bridge" {
				sb.network.Cleanup(log)
			}
		} else {
			sboxes = append(sboxes, sb)
		}
	}
	sbox.daemon.sandboxes = sboxes
}

func (sbox *Sandbox) logMessages() {
	scanner := bufio.NewScanner(sbox.stderr)
	seenOk := false
	for scanner.Scan() {
		line := scanner.Text()
		if line == "OK" && !seenOk {
			sbox.daemon.log.Info("oz-init (%s) is ready", sbox.profile.Name)
			seenOk = true
			sbox.ready.Done()
		} else if len(line) > 1 {
			sbox.logLine(line)
		}
	}
	sbox.stderr.Close()
}

func (sbox *Sandbox) logLine(line string) {
	if len(line) < 2 {
		return
	}
	f := sbox.getLogFunc(line[0])
	msg := line[2:]
	if f != nil {
		f("[%s] %s", sbox.profile.Name, msg)
	} else {
		sbox.daemon.log.Info("[%s] %s", sbox.profile.Name, line)
	}
}

func (sbox *Sandbox) getLogFunc(c byte) func(string, ...interface{}) {
	log := sbox.daemon.log
	switch c {
	case 'D':
		return log.Debug
	case 'I':
		return log.Info
	case 'N':
		return log.Notice
	case 'W':
		return log.Warning
	case 'E':
		return log.Error
	case 'C':
		return log.Critical
	}
	return nil
}

func (sbox *Sandbox) startXpraClient() {
	sbox.xpra = xpra.NewClient(
		&sbox.profile.XServer,
		uint64(sbox.display),
		sbox.cred,
		sbox.fs.Xpra(),
		sbox.profile.Name,
		sbox.daemon.log)
	sbox.xpra.Process.Stdout = os.Stdout
	sbox.xpra.Process.Stderr = os.Stdout
	if err := sbox.xpra.Process.Start(); err != nil {
		sbox.daemon.Warning("Failed to start xpra client: %v", err)
	}
}
<commit_msg>Fixed launching container with "host" network (ie: no network ns)<commit_after>package daemon

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"path"
	"sync"
	"syscall"

	"github.com/subgraph/oz"
	"github.com/subgraph/oz/fs"
	"github.com/subgraph/oz/xpra"
	"github.com/subgraph/oz/network"

	"github.com/op/go-logging"
)

const initPath = "/usr/local/bin/oz-init"

type Sandbox struct {
	daemon  *daemonState
	id      int
	display int
	profile *oz.Profile
	init    *exec.Cmd
	cred    *syscall.Credential
	fs      *fs.Filesystem
	stderr  io.ReadCloser
	addr    string
	xpra    *xpra.Xpra
	ready   sync.WaitGroup
	network *network.SandboxNetwork
}

/*
func findSandbox(id int) *Sandbox {
	for _, sb := range sandboxes {
		if sb.id == id {
			return sb
		}
	}
	return nil
}
*/

func createInitCommand(name, chroot string, uid uint32, display int, stn *network.SandboxNetwork, nettype string) *exec.Cmd {
	cmd := exec.Command(initPath)
	cmd.Dir = "/"

	cloneFlags := uintptr(syscall.CLONE_NEWNS)
	cloneFlags |= syscall.CLONE_NEWIPC
	cloneFlags |= syscall.CLONE_NEWPID
	cloneFlags |= syscall.CLONE_NEWUTS

	if nettype != "host" {
		cloneFlags |= syscall.CLONE_NEWNET
	}

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Chroot:     chroot,
		Cloneflags: cloneFlags,
	}
	cmd.Env = []string{
		"INIT_PROFILE=" + name,
		fmt.Sprintf("INIT_UID=%d", uid),
	}

	if stn.Ip != "" {
		cmd.Env = append(cmd.Env, "INIT_ADDR=" + stn.Ip)
		cmd.Env = append(cmd.Env, "INIT_VHOST=" + stn.VethHost)
		cmd.Env = append(cmd.Env, "INIT_VGUEST=" + stn.VethGuest)
		cmd.Env = append(cmd.Env, "INIT_GATEWAY=" + stn.Gateway.String() + "/" + stn.Class)
	}

	if display > 0 {
		cmd.Env = append(cmd.Env, fmt.Sprintf("INIT_DISPLAY=%d", display))
	}

	return cmd
}

func (d *daemonState) launch(p *oz.Profile, uid, gid uint32, log *logging.Logger) (*Sandbox, error) {
	u, err := user.LookupId(fmt.Sprintf("%d", uid))
	if err != nil {
		return nil, fmt.Errorf("failed to lookup user for uid=%d: %v", uid, err)
	}
	fs := fs.NewFromProfile(p, u, d.config.SandboxPath, d.log)
	if err := fs.Setup(d.config.ProfileDir); err != nil {
		return nil, err
	}
	display := 0
	if p.XServer.Enabled  && p.Networking.Nettype == "host" {
		display = d.nextDisplay
		d.nextDisplay += 1
	}

	stn := new(network.SandboxNetwork)
	if p.Networking.Nettype == "bridge" {
		stn, err = network.PrepareSandboxNetwork(d.network, log)
		if err != nil {
			return nil, fmt.Errorf("Unable to prepare veth network: %+v", err)
		}
	}

	cmd := createInitCommand(p.Name, fs.Root(), uid, display, stn, p.Networking.Nettype)
	log.Debug("Command environment: %+v", cmd.Env)
	pp, err := cmd.StderrPipe()
	if err != nil {
		fs.Cleanup()
		return nil, fmt.Errorf("error creating stderr pipe for init process: %v", err)

	}

	if err := cmd.Start(); err != nil {
		fs.Cleanup()
		return nil, fmt.Errorf("Unable to start process: %+v", err)
	}

	sbox := &Sandbox{
		daemon:  d,
		id:      d.nextSboxId,
		display: display,
		profile: p,
		init:    cmd,
		cred:    &syscall.Credential{Uid: uid, Gid: gid},
		fs:      fs,
		addr:    path.Join(fs.Root(), "tmp", "oz-init-control"),
		stderr:  pp,
		network: stn,
	}

	if p.Networking.Nettype == "bridge" {
		if err := network.NetInit(stn, d.network, cmd.Process.Pid, log); err != nil {
			cmd.Process.Kill()
			fs.Cleanup()
			return nil, fmt.Errorf("Unable to create veth networking: %+v", err)
		}
	}

	sbox.ready.Add(1)
	go sbox.logMessages()
	if sbox.profile.XServer.Enabled {
		go func() {
			sbox.ready.Wait()
			go sbox.startXpraClient()
		}()
	}
	d.nextSboxId += 1
	d.sandboxes = append(d.sandboxes, sbox)
	return sbox, nil
}

func (sbox *Sandbox) remove(log *logging.Logger) {
	sboxes := []*Sandbox{}
	for _, sb := range sbox.daemon.sandboxes {
		if sb == sbox {
			sb.fs.Cleanup()
			if sb.profile.Networking.Nettype == "bridge" {
				sb.network.Cleanup(log)
			}
		} else {
			sboxes = append(sboxes, sb)
		}
	}
	sbox.daemon.sandboxes = sboxes
}

func (sbox *Sandbox) logMessages() {
	scanner := bufio.NewScanner(sbox.stderr)
	seenOk := false
	for scanner.Scan() {
		line := scanner.Text()
		if line == "OK" && !seenOk {
			sbox.daemon.log.Info("oz-init (%s) is ready", sbox.profile.Name)
			seenOk = true
			sbox.ready.Done()
		} else if len(line) > 1 {
			sbox.logLine(line)
		}
	}
	sbox.stderr.Close()
}

func (sbox *Sandbox) logLine(line string) {
	if len(line) < 2 {
		return
	}
	f := sbox.getLogFunc(line[0])
	msg := line[2:]
	if f != nil {
		f("[%s] %s", sbox.profile.Name, msg)
	} else {
		sbox.daemon.log.Info("[%s] %s", sbox.profile.Name, line)
	}
}

func (sbox *Sandbox) getLogFunc(c byte) func(string, ...interface{}) {
	log := sbox.daemon.log
	switch c {
	case 'D':
		return log.Debug
	case 'I':
		return log.Info
	case 'N':
		return log.Notice
	case 'W':
		return log.Warning
	case 'E':
		return log.Error
	case 'C':
		return log.Critical
	}
	return nil
}

func (sbox *Sandbox) startXpraClient() {
	sbox.xpra = xpra.NewClient(
		&sbox.profile.XServer,
		uint64(sbox.display),
		sbox.cred,
		sbox.fs.Xpra(),
		sbox.profile.Name,
		sbox.daemon.log)
	sbox.xpra.Process.Stdout = os.Stdout
	sbox.xpra.Process.Stderr = os.Stdout
	if err := sbox.xpra.Process.Start(); err != nil {
		sbox.daemon.Warning("Failed to start xpra client: %v", err)
	}
}
<|endoftext|>


<commit_before>package azurerm

import (
	"strings"

	"github.com/Azure/azure-sdk-for-go/arm/containerinstance"
	"github.com/hashicorp/terraform/helper/schema"
)

func resourceArmContainerGroup() *schema.Resource {
	return &schema.Resource{
		Create: resourceArmContainerGroupCreate,
		Read:   resourceArmContainerGroupRead,
		Update: resourceArmContainerGroupCreate,
		Delete: resourceArmContainerGroupDelete,

		Schema: map[string]*schema.Schema{
			"name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"location": locationSchema(),

			"resource_group_name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"image": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"cpu": {
				Type:     schema.TypeFloat,
				Optional: true,
				ForceNew: true,
			},

			"ip_address_type": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"memory": {
				Type:     schema.TypeFloat,
				Optional: true,
				ForceNew: true,
			},

			"os_type": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"port": {
				Type:     schema.TypeInt,
				Optional: true,
				ForceNew: true,
			},

			"protocol": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"tags": tagsSchema(),
		},
	}
}

func resourceArmContainerGroupCreate(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containterGroupsClient := client.containerGroupsClient

	// container group properties
	resGroup := d.Get("resource_group_name").(string)
	name := d.Get("name").(string)
	location := d.Get("location").(string)
	OSType := d.Get("os_type").(string)
	IPAddressType := d.Get("ip_address_type").(string)
	protocol := d.Get("protocol").(string)
	tags := d.Get("tags").(map[string]interface{})

	// per container properties
	image := d.Get("image").(string)
	cpu := d.Get("cpu").(float64)
	memory := d.Get("memory").(float64)
	port := d.Get("port").(int32)

	// type ContainerGroupProperties struct {
	// 	ProvisioningState        *string                    `json:"provisioningState,omitempty"`
	// 	Containers               *[]Container               `json:"containers,omitempty"`
	// 	ImageRegistryCredentials *[]ImageRegistryCredential `json:"imageRegistryCredentials,omitempty"`
	// 	RestartPolicy            ContainerRestartPolicy     `json:"restartPolicy,omitempty"`
	// 	IPAddress                *IPAddress                 `json:"ipAddress,omitempty"`
	// 	OsType                   OperatingSystemTypes       `json:"osType,omitempty"`
	// 	State                    *string                    `json:"state,omitempty"`
	// 	Volumes                  *[]Volume                  `json:"volumes,omitempty"`
	// }

	// type ContainerProperties struct {
	// 	Image                *string                          `json:"image,omitempty"`
	// 	Command              *[]string                        `json:"command,omitempty"`
	// 	Ports                *[]ContainerPort                 `json:"ports,omitempty"`
	// 	EnvironmentVariables *[]EnvironmentVariable           `json:"environmentVariables,omitempty"`
	// 	InstanceView         *ContainerPropertiesInstanceView `json:"instanceView,omitempty"`
	// 	Resources            *ResourceRequirements            `json:"resources,omitempty"`
	// 	VolumeMounts         *[]VolumeMount                   `json:"volumeMounts,omitempty"`
	// }

	// per container port (port number only)
	containerPort := containerinstance.ContainerPort{
		Port: &port,
	}

	container := containerinstance.Container{
		Name: &name,
		ContainerProperties: &containerinstance.ContainerProperties{
			Image: &image,
			Ports: &[]containerinstance.ContainerPort{containerPort},
			Resources: &containerinstance.ResourceRequirements{
				Requests: &containerinstance.ResourceRequests{
					MemoryInGB: &memory,
					CPU:        &cpu,
				},
			},
		},
	}

	// container group port (port number + protocol)
	containerGroupPort := containerinstance.Port{
		Port: &port,
	}

	if strings.ToUpper(protocol) == "TCP" || strings.ToUpper(protocol) == "UDP" {
		containerGroupPort.Protocol = containerinstance.ContainerGroupNetworkProtocol(strings.ToUpper(protocol))
	}

	containerGroup := containerinstance.ContainerGroup{
		Name:     &name,
		Type:     &OSType,
		Location: &location,
		Tags:     expandTags(tags),
		ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
			Containers: &[]containerinstance.Container{container},
			IPAddress: &containerinstance.IPAddress{
				Type:  &IPAddressType,
				Ports: &[]containerinstance.Port{containerGroupPort},
			},
		},
	}

	_, error := containterGroupsClient.CreateOrUpdate(resGroup, name, containerGroup)
	if error != nil {
		return error
	}

	return nil
}
func resourceArmContainerGroupRead(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containterGroupsClient := client.containerGroupsClient

	// container group properties
	resGroup := d.Get("resource_group_name").(string)
	name := d.Get("name").(string)

	_, error := containterGroupsClient.Get(resGroup, name)

	if error != nil {
		return error
	}

	return nil
}
func resourceArmContainerGroupDelete(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containterGroupsClient := client.containerGroupsClient

	// container group properties
	resGroup := d.Get("resource_group_name").(string)
	name := d.Get("name").(string)

	_, error := containterGroupsClient.Delete(resGroup, name)

	if error != nil {
		return error
	}

	return nil
}
<commit_msg>WIP: Container groups resource<commit_after>package azurerm

import (
	"fmt"
	"strings"

	"github.com/Azure/azure-sdk-for-go/arm/containerinstance"
	"github.com/hashicorp/terraform/helper/schema"
)

func resourceArmContainerGroup() *schema.Resource {
	return &schema.Resource{
		Create: resourceArmContainerGroupCreate,
		Read:   resourceArmContainerGroupRead,
		Update: resourceArmContainerGroupCreate,
		Delete: resourceArmContainerGroupDelete,

		Schema: map[string]*schema.Schema{
			"name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"location": locationSchema(),

			"resource_group_name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"image": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"cpu": {
				Type:     schema.TypeFloat,
				Optional: true,
				ForceNew: true,
			},

			"ip_address_type": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"memory": {
				Type:     schema.TypeFloat,
				Optional: true,
				ForceNew: true,
			},

			"os_type": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"port": {
				Type:     schema.TypeInt,
				Optional: true,
				ForceNew: true,
			},

			"protocol": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"ip_address": {
				Type:     schema.TypeString,
				Computed: true,
			},

			"tags": tagsSchema(),
		},
	}
}

func resourceArmContainerGroupCreate(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containerGroupsClient := client.containerGroupsClient

	// container group properties
	resGroup := d.Get("resource_group_name").(string)
	name := d.Get("name").(string)
	location := d.Get("location").(string)
	OSType := d.Get("os_type").(string)
	IPAddressType := d.Get("ip_address_type").(string)
	protocol := d.Get("protocol").(string)
	tags := d.Get("tags").(map[string]interface{})

	// per container properties
	image := d.Get("image").(string)
	cpu := d.Get("cpu").(float64)
	memory := d.Get("memory").(float64)
	port := d.Get("port").(int32)

	// type ContainerGroupProperties struct {
	// 	ProvisioningState        *string                    `json:"provisioningState,omitempty"`
	// 	Containers               *[]Container               `json:"containers,omitempty"`
	// 	ImageRegistryCredentials *[]ImageRegistryCredential `json:"imageRegistryCredentials,omitempty"`
	// 	RestartPolicy            ContainerRestartPolicy     `json:"restartPolicy,omitempty"`
	// 	IPAddress                *IPAddress                 `json:"ipAddress,omitempty"`
	// 	OsType                   OperatingSystemTypes       `json:"osType,omitempty"`
	// 	State                    *string                    `json:"state,omitempty"`
	// 	Volumes                  *[]Volume                  `json:"volumes,omitempty"`
	// }

	// type ContainerProperties struct {
	// 	Image                *string                          `json:"image,omitempty"`
	// 	Command              *[]string                        `json:"command,omitempty"`
	// 	Ports                *[]ContainerPort                 `json:"ports,omitempty"`
	// 	EnvironmentVariables *[]EnvironmentVariable           `json:"environmentVariables,omitempty"`
	// 	InstanceView         *ContainerPropertiesInstanceView `json:"instanceView,omitempty"`
	// 	Resources            *ResourceRequirements            `json:"resources,omitempty"`
	// 	VolumeMounts         *[]VolumeMount                   `json:"volumeMounts,omitempty"`
	// }

	// per container port (port number only)
	containerPort := containerinstance.ContainerPort{
		Port: &port,
	}

	container := containerinstance.Container{
		Name: &name,
		ContainerProperties: &containerinstance.ContainerProperties{
			Image: &image,
			Ports: &[]containerinstance.ContainerPort{containerPort},
			Resources: &containerinstance.ResourceRequirements{
				Requests: &containerinstance.ResourceRequests{
					MemoryInGB: &memory,
					CPU:        &cpu,
				},
			},
		},
	}

	// container group port (port number + protocol)
	containerGroupPort := containerinstance.Port{
		Port: &port,
	}

	if strings.ToUpper(protocol) == "TCP" || strings.ToUpper(protocol) == "UDP" {
		containerGroupPort.Protocol = containerinstance.ContainerGroupNetworkProtocol(strings.ToUpper(protocol))
	}

	containerGroup := containerinstance.ContainerGroup{
		Name:     &name,
		Type:     &OSType,
		Location: &location,
		Tags:     expandTags(tags),
		ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
			Containers: &[]containerinstance.Container{container},
			IPAddress: &containerinstance.IPAddress{
				Type:  &IPAddressType,
				Ports: &[]containerinstance.Port{containerGroupPort},
			},
		},
	}

	_, error := containerGroupsClient.CreateOrUpdate(resGroup, name, containerGroup)
	if error != nil {
		return error
	}

	read, readErr := containerGroupsClient.Get(resGroup, name)
	if readErr != nil {
		return readErr
	}

	if read.ID == nil {
		return fmt.Errorf("Cannot read container group %s (resource group %s) ID", name, resGroup)
	}

	d.SetId(*read.ID)

	return nil
}
func resourceArmContainerGroupRead(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containterGroupsClient := client.containerGroupsClient

	id, err := parseAzureResourceID(d.Id())

	if err != nil {
		return err
	}

	resGroup := id.ResourceGroup
	name := id.Path["containergroups"]

	resp, error := containterGroupsClient.Get(resGroup, name)

	if error != nil {
		return error
	}

	d.Set("name", name)
	d.Set("resource_group_name", resGroup)
	d.Set("location", azureRMNormalizeLocation(*resp.Location))
	flattenAndSetTags(d, resp.Tags)

	d.Set("os_type", string(resp.OsType))
	d.Set("ip_address_type", *resp.IPAddress.Type)
	d.Set("ip_address", *resp.IPAddress.IP)

	ports := *resp.IPAddress.Ports
	d.Set("protocol", string(ports[0].Protocol))

	containers := *resp.Containers
	d.Set("image", containers[0].Image)
	resourceRequirements := *containers[0].Resources
	resourceRequests := *resourceRequirements.Requests
	d.Set("cpu", *resourceRequests.CPU)
	d.Set("memory", *resourceRequests.MemoryInGB)
	containerPorts := *containers[0].Ports
	d.Set("port", containerPorts[0].Port)

	return nil
}
func resourceArmContainerGroupDelete(d *schema.ResourceData, meta interface{}) error {
	client := meta.(*ArmClient)
	containterGroupsClient := client.containerGroupsClient

	// container group properties
	resGroup := d.Get("resource_group_name").(string)
	name := d.Get("name").(string)

	_, error := containterGroupsClient.Delete(resGroup, name)

	if error != nil {
		return error
	}

	return nil
}
<|endoftext|>