Commit e8d13594 authored by Victor Marmol's avatar Victor Marmol

Add HTTP cAdvisor dependencies.

Also removed unused goamz.
parent 6d465c4d
...@@ -55,6 +55,10 @@ ...@@ -55,6 +55,10 @@
"Rev": "51fe59aca108dc5680109e7b2051cbdcfa5a253c" "Rev": "51fe59aca108dc5680109e7b2051cbdcfa5a253c"
}, },
{ {
"ImportPath": "github.com/abbot/go-http-auth",
"Rev": "c0ef4539dfab4d21c8ef20ba2924f9fc6f186d35"
},
{
"ImportPath": "github.com/beorn7/perks/quantile", "ImportPath": "github.com/beorn7/perks/quantile",
"Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d" "Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d"
}, },
...@@ -180,59 +184,84 @@ ...@@ -180,59 +184,84 @@
"Rev": "7f07925444bb51fa4cf9dfe6f7661876f8852275" "Rev": "7f07925444bb51fa4cf9dfe6f7661876f8852275"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/api",
"Comment": "0.10.1-42-g4e2479b",
"Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
},
{
"ImportPath": "github.com/google/cadvisor/client", "ImportPath": "github.com/google/cadvisor/client",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/container", "ImportPath": "github.com/google/cadvisor/container",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/events", "ImportPath": "github.com/google/cadvisor/events",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/fs", "ImportPath": "github.com/google/cadvisor/fs",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
},
{
"ImportPath": "github.com/google/cadvisor/healthz",
"Comment": "0.10.1-42-g4e2479b",
"Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
},
{
"ImportPath": "github.com/google/cadvisor/http",
"Comment": "0.10.1-42-g4e2479b",
"Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/info/v1", "ImportPath": "github.com/google/cadvisor/info/v1",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/info/v2", "ImportPath": "github.com/google/cadvisor/info/v2",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/manager", "ImportPath": "github.com/google/cadvisor/manager",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
},
{
"ImportPath": "github.com/google/cadvisor/pages",
"Comment": "0.10.1-42-g4e2479b",
"Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/storage", "ImportPath": "github.com/google/cadvisor/storage",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/summary", "ImportPath": "github.com/google/cadvisor/summary",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/utils", "ImportPath": "github.com/google/cadvisor/utils",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
},
{
"ImportPath": "github.com/google/cadvisor/validate",
"Comment": "0.10.1-42-g4e2479b",
"Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/cadvisor/version", "ImportPath": "github.com/google/cadvisor/version",
"Comment": "0.10.1-36-g62a1788", "Comment": "0.10.1-42-g4e2479b",
"Rev": "62a1788621f4adee2dbf08c26060ed7fb8c0297d" "Rev": "4e2479bcabe7af08825066f5ece2122553562b34"
}, },
{ {
"ImportPath": "github.com/google/gofuzz", "ImportPath": "github.com/google/gofuzz",
...@@ -265,10 +294,6 @@ ...@@ -265,10 +294,6 @@
"Rev": "703cfb45985762869e465f37ed030ff01615ff1e" "Rev": "703cfb45985762869e465f37ed030ff01615ff1e"
}, },
{ {
"ImportPath": "github.com/mitchellh/goamz/elb",
"Rev": "703cfb45985762869e465f37ed030ff01615ff1e"
},
{
"ImportPath": "github.com/mitchellh/mapstructure", "ImportPath": "github.com/mitchellh/mapstructure",
"Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf" "Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf"
}, },
......
include $(GOROOT)/src/Make.inc
TARG=auth_digest
GOFILES=\
auth.go\
digest.go\
basic.go\
misc.go\
md5crypt.go\
users.go\
include $(GOROOT)/src/Make.pkg
HTTP Authentication implementation in Go
========================================
This is an implementation of HTTP Basic and HTTP Digest authentication
in Go language. It is designed as a simple wrapper for
http.RequestHandler functions.
Features
--------
* Supports HTTP Basic and HTTP Digest authentication.
* Supports htpasswd and htdigest formatted files.
* Automatic reloading of password files.
* Pluggable interface for user/password storage.
* Supports MD5 and SHA1 for Basic authentication password storage.
* Configurable Digest nonce cache size with expiration.
* Wrapper for legacy http handlers (http.HandlerFunc interface)
Example usage
-------------
This is a complete working example for Basic auth:
package main
import (
auth "github.com/abbot/go-http-auth"
"fmt"
"net/http"
)
func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1"
}
return ""
}
func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", r.Username)
}
func main() {
authenticator := auth.NewBasicAuthenticator("example.com", Secret)
http.HandleFunc("/", authenticator.Wrap(handle))
http.ListenAndServe(":8080", nil)
}
See more examples in the "examples" directory.
Legal
-----
This module is developed under Apache 2.0 license, and can be used for
open and proprietary projects.
Copyright 2012-2013 Lev Shamardin
Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file or any other part of this project 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 auth
import "net/http"
/*
Request handlers must take AuthenticatedRequest instead of http.Request
*/
type AuthenticatedRequest struct {
http.Request
/*
Authenticated user name. Current API implies that Username is
never empty, which means that authentication is always done
before calling the request handler.
*/
Username string
}
/*
AuthenticatedHandlerFunc is like http.HandlerFunc, but takes
AuthenticatedRequest instead of http.Request
*/
type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest)
/*
Authenticator wraps an AuthenticatedHandlerFunc with
authentication-checking code.
Typical Authenticator usage is something like:
authenticator := SomeAuthenticator(...)
http.HandleFunc("/", authenticator(my_handler))
Authenticator wrapper checks the user authentication and calls the
wrapped function only after authentication has succeeded. Otherwise,
it returns a handler which initiates the authentication procedure.
*/
type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc
type AuthenticatorInterface interface {
Wrap(AuthenticatedHandlerFunc) http.HandlerFunc
}
func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc {
return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
ar.Header.Set("X-Authenticated-Username", ar.Username)
wrapped(w, &ar.Request)
})
}
package auth
import (
"crypto/sha1"
"encoding/base64"
"net/http"
"strings"
)
type BasicAuth struct {
Realm string
Secrets SecretProvider
}
/*
Checks the username/password combination from the request. Returns
either an empty string (authentication failed) or the name of the
authenticated user.
Supports MD5 and SHA1 password entries
*/
func (a *BasicAuth) CheckAuth(r *http.Request) string {
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 || s[0] != "Basic" {
return ""
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
return ""
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return ""
}
passwd := a.Secrets(pair[0], a.Realm)
if passwd == "" {
return ""
}
if strings.HasPrefix(passwd, "{SHA}") {
d := sha1.New()
d.Write([]byte(pair[1]))
if passwd[5:] != base64.StdEncoding.EncodeToString(d.Sum(nil)) {
return ""
}
} else {
e := NewMD5Entry(passwd)
if e == nil {
return ""
}
if passwd != string(MD5Crypt([]byte(pair[1]), e.Salt, e.Magic)) {
return ""
}
}
return pair[0]
}
/*
http.Handler for BasicAuth which initiates the authentication process
(or requires reauthentication).
*/
func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Basic realm="`+a.Realm+`"`)
w.WriteHeader(401)
w.Write([]byte("401 Unauthorized\n"))
}
/*
BasicAuthenticator returns a function, which wraps an
AuthenticatedHandlerFunc converting it to http.HandlerFunc. This
wrapper function checks the authentication and either sends back
required authentication headers, or calls the wrapped function with
authenticated username in the AuthenticatedRequest.
*/
func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username := a.CheckAuth(r); username == "" {
a.RequireAuth(w, r)
} else {
ar := &AuthenticatedRequest{Request: *r, Username: username}
wrapped(w, ar)
}
}
}
func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth {
return &BasicAuth{Realm: realm, Secrets: secrets}
}
package auth
import (
"encoding/base64"
"net/http"
"testing"
)
func TestAuthBasic(t *testing.T) {
secrets := HtpasswdFileProvider("test.htpasswd")
a := &BasicAuth{Realm: "example.com", Secrets: secrets}
r := &http.Request{}
r.Method = "GET"
if a.CheckAuth(r) != "" {
t.Fatal("CheckAuth passed on empty headers")
}
r.Header = http.Header(make(map[string][]string))
r.Header.Set("Authorization", "Digest blabla ololo")
if a.CheckAuth(r) != "" {
t.Fatal("CheckAuth passed on bad headers")
}
r.Header.Set("Authorization", "Basic !@#")
if a.CheckAuth(r) != "" {
t.Fatal("CheckAuth passed on bad base64 data")
}
data := [][]string{
{"test", "hello"},
{"test2", "hello2"},
}
for _, tc := range data {
auth := base64.StdEncoding.EncodeToString([]byte(tc[0] + ":" + tc[1]))
r.Header.Set("Authorization", "Basic "+auth)
if a.CheckAuth(r) != tc[0] {
t.Fatalf("CheckAuth failed for user '%s'", tc[0])
}
}
}
package auth
import (
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type digest_client struct {
nc uint64
last_seen int64
}
type DigestAuth struct {
Realm string
Opaque string
Secrets SecretProvider
PlainTextSecrets bool
/*
Approximate size of Client's Cache. When actual number of
tracked client nonces exceeds
ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2
older entries are purged.
*/
ClientCacheSize int
ClientCacheTolerance int
clients map[string]*digest_client
mutex sync.Mutex
}
type digest_cache_entry struct {
nonce string
last_seen int64
}
type digest_cache []digest_cache_entry
func (c digest_cache) Less(i, j int) bool {
return c[i].last_seen < c[j].last_seen
}
func (c digest_cache) Len() int {
return len(c)
}
func (c digest_cache) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
/*
Remove count oldest entries from DigestAuth.clients
*/
func (a *DigestAuth) Purge(count int) {
entries := make([]digest_cache_entry, 0, len(a.clients))
for nonce, client := range a.clients {
entries = append(entries, digest_cache_entry{nonce, client.last_seen})
}
cache := digest_cache(entries)
sort.Sort(cache)
for _, client := range cache[:count] {
delete(a.clients, client.nonce)
}
}
/*
http.Handler for DigestAuth which initiates the authentication process
(or requires reauthentication).
*/
func (a *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance {
a.Purge(a.ClientCacheTolerance * 2)
}
nonce := RandomKey()
a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
w.Header().Set("WWW-Authenticate",
fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`,
a.Realm, nonce, a.Opaque))
w.WriteHeader(401)
w.Write([]byte("401 Unauthorized\n"))
}
/*
Parse Authorization header from the http.Request. Returns a map of
auth parameters or nil if the header is not a valid parsable Digest
auth header.
*/
func DigestAuthParams(r *http.Request) map[string]string {
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 || s[0] != "Digest" {
return nil
}
result := map[string]string{}
for _, kv := range strings.Split(s[1], ",") {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
continue
}
result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
}
return result
}
/*
Check if request contains valid authentication data. Returns a pair
of username, authinfo where username is the name of the authenticated
user or an empty string and authinfo is the contents for the optional
Authentication-Info response header.
*/
func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) {
da.mutex.Lock()
defer da.mutex.Unlock()
username = ""
authinfo = nil
auth := DigestAuthParams(r)
if auth == nil || da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" {
return
}
// Check if the requested URI matches auth header
switch u, err := url.Parse(auth["uri"]); {
case err != nil:
return
case r.URL == nil:
return
case len(u.Path) > len(r.URL.Path):
return
case !strings.HasPrefix(r.URL.Path, u.Path):
return
}
HA1 := da.Secrets(auth["username"], da.Realm)
if da.PlainTextSecrets {
HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1)
}
HA2 := H(r.Method + ":" + auth["uri"])
KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":"))
if KD != auth["response"] {
return
}
// At this point crypto checks are completed and validated.
// Now check if the session is valid.
nc, err := strconv.ParseUint(auth["nc"], 16, 64)
if err != nil {
return
}
if client, ok := da.clients[auth["nonce"]]; !ok {
return
} else {
if client.nc != 0 && client.nc >= nc {
return
}
client.nc = nc
client.last_seen = time.Now().UnixNano()
}
resp_HA2 := H(":" + auth["uri"])
rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], resp_HA2}, ":"))
info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"])
return auth["username"], &info
}
/*
Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth
*/
const DefaultClientCacheSize = 1000
const DefaultClientCacheTolerance = 100
/*
Wrap returns an Authenticator which uses HTTP Digest
authentication. Arguments:
realm: The authentication realm.
secrets: SecretProvider which must return HA1 digests for the same
realm as above.
*/
func (a *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username, authinfo := a.CheckAuth(r); username == "" {
a.RequireAuth(w, r)
} else {
ar := &AuthenticatedRequest{Request: *r, Username: username}
if authinfo != nil {
w.Header().Set("Authentication-Info", *authinfo)
}
wrapped(w, ar)
}
}
}
/*
JustCheck returns function which converts an http.HandlerFunc into a
http.HandlerFunc which requires authentication. Username is passed as
an extra X-Authenticated-Username header.
*/
func (a *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc {
return a.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
ar.Header.Set("X-Authenticated-Username", ar.Username)
wrapped(w, &ar.Request)
})
}
func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth {
da := &DigestAuth{
Opaque: RandomKey(),
Realm: realm,
Secrets: secrets,
PlainTextSecrets: false,
ClientCacheSize: DefaultClientCacheSize,
ClientCacheTolerance: DefaultClientCacheTolerance,
clients: map[string]*digest_client{}}
return da
}
package auth
import (
"net/http"
"net/url"
"testing"
"time"
)
func TestAuthDigest(t *testing.T) {
secrets := HtdigestFileProvider("test.htdigest")
da := &DigestAuth{Opaque: "U7H+ier3Ae8Skd/g",
Realm: "example.com",
Secrets: secrets,
clients: map[string]*digest_client{}}
r := &http.Request{}
r.Method = "GET"
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for empty request header")
}
r.Header = http.Header(make(map[string][]string))
r.Header.Set("Authorization", "Digest blabla")
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for bad request header")
}
r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="Vb9BP/h81n3GpTTB", uri="/t2", cnonce="NjE4MTM2", nc=00000001, qop="auth", response="ffc357c4eba74773c8687e0bc724c9a3", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`)
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for unknown client")
}
r.URL, _ = url.Parse("/t2")
da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
if u, _ := da.CheckAuth(r); u != "test" {
t.Fatal("empty auth for legitimate client")
}
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for outdated nc")
}
r.URL, _ = url.Parse("/")
da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for bad request path")
}
r.URL, _ = url.Parse("/t3")
da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
if u, _ := da.CheckAuth(r); u != "" {
t.Fatal("non-empty auth for bad request path")
}
da.clients["+RbVXSbIoa1SaJk1"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="+RbVXSbIoa1SaJk1", uri="/", cnonce="NjE4NDkw", nc=00000001, qop="auth", response="c08918024d7faaabd5424654c4e3ad1c", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`)
if u, _ := da.CheckAuth(r); u != "test" {
t.Fatal("empty auth for valid request in subpath")
}
}
// +build ignore
/*
Example application using Basic auth
Build with:
go build basic.go
*/
package main
import (
auth ".."
"fmt"
"net/http"
)
func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1"
}
return ""
}
func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", r.Username)
}
func main() {
authenticator := auth.NewBasicAuthenticator("example.com", Secret)
http.HandleFunc("/", authenticator.Wrap(handle))
http.ListenAndServe(":8080", nil)
}
// +build ignore
/*
Example application using Digest auth
Build with:
go build digest.go
*/
package main
import (
auth ".."
"fmt"
"net/http"
)
func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "b98e16cbc3d01734b264adba7baa3bf9"
}
return ""
}
func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", r.Username)
}
func main() {
authenticator := auth.NewDigestAuthenticator("example.com", Secret)
http.HandleFunc("/", authenticator.Wrap(handle))
http.ListenAndServe(":8080", nil)
}
// +build ignore
/*
Example demonstrating how to wrap an application which is unaware of
authenticated requests with a "pass-through" authentication
Build with:
go build wrapped.go
*/
package main
import (
auth ".."
"fmt"
"net/http"
)
func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1"
}
return ""
}
func regular_handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<html><body><h1>This application is unaware of authentication</h1></body></html>")
}
func main() {
authenticator := auth.NewBasicAuthenticator("example.com", Secret)
http.HandleFunc("/", auth.JustCheck(authenticator, regular_handler))
http.ListenAndServe(":8080", nil)
}
package auth
import "crypto/md5"
import "strings"
const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var md5_crypt_swaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11}
type MD5Entry struct {
Magic, Salt, Hash []byte
}
func NewMD5Entry(e string) *MD5Entry {
parts := strings.SplitN(e, "$", 4)
if len(parts) != 4 {
return nil
}
return &MD5Entry{
Magic: []byte("$" + parts[1] + "$"),
Salt: []byte(parts[2]),
Hash: []byte(parts[3]),
}
}
/*
MD5 password crypt implementation
*/
func MD5Crypt(password, salt, magic []byte) []byte {
d := md5.New()
d.Write(password)
d.Write(magic)
d.Write(salt)
d2 := md5.New()
d2.Write(password)
d2.Write(salt)
d2.Write(password)
for i, mixin := 0, d2.Sum(nil); i < len(password); i++ {
d.Write([]byte{mixin[i%16]})
}
for i := len(password); i != 0; i >>= 1 {
if i&1 == 0 {
d.Write([]byte{password[0]})
} else {
d.Write([]byte{0})
}
}
final := d.Sum(nil)
for i := 0; i < 1000; i++ {
d2 := md5.New()
if i&1 == 0 {
d2.Write(final)
} else {
d2.Write(password)
}
if i%3 != 0 {
d2.Write(salt)
}
if i%7 != 0 {
d2.Write(password)
}
if i&1 == 0 {
d2.Write(password)
} else {
d2.Write(final)
}
final = d2.Sum(nil)
}
result := make([]byte, 0, 22)
v := uint(0)
bits := uint(0)
for _, i := range md5_crypt_swaps {
v |= (uint(final[i]) << bits)
for bits = bits + 8; bits > 6; bits -= 6 {
result = append(result, itoa64[v&0x3f])
v >>= 6
}
}
result = append(result, itoa64[v&0x3f])
return append(append(append(magic, salt...), '$'), result...)
}
package auth
import "testing"
func Test_MD5Crypt(t *testing.T) {
test_cases := [][]string{
{"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"},
{"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"},
}
for _, tc := range test_cases {
e := NewMD5Entry(tc[1])
result := MD5Crypt([]byte(tc[0]), e.Salt, e.Magic)
if string(result) != tc[1] {
t.Fatalf("MD5Crypt returned '%s' instead of '%s'", string(result), tc[1])
}
t.Logf("MD5Crypt: '%s' (%s%s$) -> %s", tc[0], e.Magic, e.Salt, result)
}
}
package auth
import "encoding/base64"
import "crypto/md5"
import "crypto/rand"
import "fmt"
/*
Return a random 16-byte base64 alphabet string
*/
func RandomKey() string {
k := make([]byte, 12)
for bytes := 0; bytes < len(k); {
n, err := rand.Read(k[bytes:])
if err != nil {
panic("rand.Read() failed")
}
bytes += n
}
return base64.StdEncoding.EncodeToString(k)
}
/*
H function for MD5 algorithm (returns a lower-case hex MD5 digest)
*/
func H(data string) string {
digest := md5.New()
digest.Write([]byte(data))
return fmt.Sprintf("%x", digest.Sum(nil))
}
package auth
import "testing"
func TestH(t *testing.T) {
const hello = "Hello, world!"
const hello_md5 = "6cd3556deb0da54bca060b4c39479839"
h := H(hello)
if h != hello_md5 {
t.Fatal("Incorrect digest for test string:", h, "instead of", hello_md5)
}
}
test:example.com:aa78524fceb0e50fd8ca96dd818b8cf9
test:{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=
test2:$apr1$a0j62R97$mYqFkloXH0/UOaUnAiV2b0
package auth
import "encoding/csv"
import "os"
/*
SecretProvider is used by authenticators. Takes user name and realm
as an argument, returns secret required for authentication (HA1 for
digest authentication, properly encrypted password for basic).
*/
type SecretProvider func(user, realm string) string
/*
Common functions for file auto-reloading
*/
type File struct {
Path string
Info os.FileInfo
/* must be set in inherited types during initialization */
Reload func()
}
func (f *File) ReloadIfNeeded() {
info, err := os.Stat(f.Path)
if err != nil {
panic(err)
}
if f.Info == nil || f.Info.ModTime() != info.ModTime() {
f.Info = info
f.Reload()
}
}
/*
Structure used for htdigest file authentication. Users map realms to
maps of users to their HA1 digests.
*/
type HtdigestFile struct {
File
Users map[string]map[string]string
}
func reload_htdigest(hf *HtdigestFile) {
r, err := os.Open(hf.Path)
if err != nil {
panic(err)
}
csv_reader := csv.NewReader(r)
csv_reader.Comma = ':'
csv_reader.Comment = '#'
csv_reader.TrimLeadingSpace = true
records, err := csv_reader.ReadAll()
if err != nil {
panic(err)
}
hf.Users = make(map[string]map[string]string)
for _, record := range records {
_, exists := hf.Users[record[1]]
if !exists {
hf.Users[record[1]] = make(map[string]string)
}
hf.Users[record[1]][record[0]] = record[2]
}
}
/*
SecretProvider implementation based on htdigest-formated files. Will
reload htdigest file on changes. Will panic on syntax errors in
htdigest files.
*/
func HtdigestFileProvider(filename string) SecretProvider {
hf := &HtdigestFile{File: File{Path: filename}}
hf.Reload = func() { reload_htdigest(hf) }
return func(user, realm string) string {
hf.ReloadIfNeeded()
_, exists := hf.Users[realm]
if !exists {
return ""
}
digest, exists := hf.Users[realm][user]
if !exists {
return ""
}
return digest
}
}
/*
Structure used for htdigest file authentication. Users map users to
their salted encrypted password
*/
type HtpasswdFile struct {
File
Users map[string]string
}
func reload_htpasswd(h *HtpasswdFile) {
r, err := os.Open(h.Path)
if err != nil {
panic(err)
}
csv_reader := csv.NewReader(r)
csv_reader.Comma = ':'
csv_reader.Comment = '#'
csv_reader.TrimLeadingSpace = true
records, err := csv_reader.ReadAll()
if err != nil {
panic(err)
}
h.Users = make(map[string]string)
for _, record := range records {
h.Users[record[0]] = record[1]
}
}
/*
SecretProvider implementation based on htpasswd-formated files. Will
reload htpasswd file on changes. Will panic on syntax errors in
htpasswd files. Realm argument of the SecretProvider is ignored.
*/
func HtpasswdFileProvider(filename string) SecretProvider {
h := &HtpasswdFile{File: File{Path: filename}}
h.Reload = func() { reload_htpasswd(h) }
return func(user, realm string) string {
h.ReloadIfNeeded()
password, exists := h.Users[user]
if !exists {
return ""
}
return password
}
}
package auth
import (
"testing"
)
func TestHtdigestFile(t *testing.T) {
secrets := HtdigestFileProvider("test.htdigest")
digest := secrets("test", "example.com")
if digest != "aa78524fceb0e50fd8ca96dd818b8cf9" {
t.Fatal("Incorrect digest for test user:", digest)
}
digest = secrets("test", "example1.com")
if digest != "" {
t.Fatal("Got digest for user in non-existant realm:", digest)
}
digest = secrets("test1", "example.com")
if digest != "" {
t.Fatal("Got digest for non-existant user:", digest)
}
}
func TestHtpasswdFile(t *testing.T) {
secrets := HtpasswdFileProvider("test.htpasswd")
passwd := secrets("test", "blah")
if passwd != "{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=" {
t.Fatal("Incorrect passwd for test user:", passwd)
}
passwd = secrets("test3", "blah")
if passwd != "" {
t.Fatal("Got passwd for non-existant user:", passwd)
}
}
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 api provides a handler for /api/
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/golang/glog"
"github.com/google/cadvisor/events"
httpMux "github.com/google/cadvisor/http/mux"
info "github.com/google/cadvisor/info/v1"
"github.com/google/cadvisor/manager"
)
const (
apiResource = "/api/"
)
func RegisterHandlers(mux httpMux.Mux, m manager.Manager) error {
apiVersions := getApiVersions()
supportedApiVersions := make(map[string]ApiVersion, len(apiVersions))
for _, v := range apiVersions {
supportedApiVersions[v.Version()] = v
}
mux.HandleFunc(apiResource, func(w http.ResponseWriter, r *http.Request) {
err := handleRequest(supportedApiVersions, m, w, r)
if err != nil {
http.Error(w, err.Error(), 500)
}
})
return nil
}
// Captures the API version, requestType [optional], and remaining request [optional].
var apiRegexp = regexp.MustCompile("/api/([^/]+)/?([^/]+)?(.*)")
const (
apiVersion = iota + 1
apiRequestType
apiRequestArgs
)
func handleRequest(supportedApiVersions map[string]ApiVersion, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
start := time.Now()
defer func() {
glog.V(2).Infof("Request took %s", time.Since(start))
}()
request := r.URL.Path
const apiPrefix = "/api"
if !strings.HasPrefix(request, apiPrefix) {
return fmt.Errorf("incomplete API request %q", request)
}
// If the request doesn't have an API version, list those.
if request == apiPrefix || request == apiResource {
versions := make([]string, 0, len(supportedApiVersions))
for v := range supportedApiVersions {
versions = append(versions, v)
}
sort.Strings(versions)
fmt.Fprintf(w, "Supported API versions: %s", strings.Join(versions, ","))
return nil
}
// Verify that we have all the elements we expect:
// /<version>/<request type>[/<args...>]
requestElements := apiRegexp.FindStringSubmatch(request)
if len(requestElements) == 0 {
return fmt.Errorf("malformed request %q", request)
}
version := requestElements[apiVersion]
requestType := requestElements[apiRequestType]
requestArgs := strings.Split(requestElements[apiRequestArgs], "/")
// Check supported versions.
versionHandler, ok := supportedApiVersions[version]
if !ok {
return fmt.Errorf("unsupported API version %q", version)
}
// If no request type, list possible request types.
if requestType == "" {
requestTypes := versionHandler.SupportedRequestTypes()
sort.Strings(requestTypes)
fmt.Fprintf(w, "Supported request types: %q", strings.Join(requestTypes, ","))
return nil
}
// Trim the first empty element from the request.
if len(requestArgs) > 0 && requestArgs[0] == "" {
requestArgs = requestArgs[1:]
}
return versionHandler.HandleRequest(requestType, requestArgs, m, w, r)
}
func writeResult(res interface{}, w http.ResponseWriter) error {
out, err := json.Marshal(res)
if err != nil {
return fmt.Errorf("failed to marshall response %+v with error: %s", res, err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(out)
return nil
}
func streamResults(results chan *events.Event, w http.ResponseWriter, r *http.Request) error {
cn, ok := w.(http.CloseNotifier)
if !ok {
return errors.New("could not access http.CloseNotifier")
}
flusher, ok := w.(http.Flusher)
if !ok {
return errors.New("could not access http.Flusher")
}
w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK)
flusher.Flush()
enc := json.NewEncoder(w)
for {
select {
case <-cn.CloseNotify():
glog.Infof("Client stopped listening")
return nil
case ev := <-results:
err := enc.Encode(ev)
if err != nil {
glog.Errorf("error encoding message %+v for result stream: %v", ev, err)
}
flusher.Flush()
}
}
}
func getContainerInfoRequest(body io.ReadCloser) (*info.ContainerInfoRequest, error) {
var query info.ContainerInfoRequest
// Default stats and samples is 64.
query.NumStats = 64
decoder := json.NewDecoder(body)
err := decoder.Decode(&query)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("unable to decode the json value: %s", err)
}
return &query, nil
}
// The user can set any or none of the following arguments in any order
// with any twice defined arguments being assigned the first value.
// If the value type for the argument is wrong the field will be assumed to be
// unassigned
// bools: historical, subcontainers, oom_events, creation_events, deletion_events
// ints: max_events, start_time (unix timestamp), end_time (unix timestamp)
// example r.URL: http://localhost:8080/api/v1.3/events?oom_events=true&historical=true&max_events=10
func getEventRequest(r *http.Request) (*events.Request, bool, error) {
query := events.NewRequest()
getHistoricalEvents := false
urlMap := r.URL.Query()
if val, ok := urlMap["historical"]; ok {
newBool, err := strconv.ParseBool(val[0])
if err == nil {
getHistoricalEvents = newBool
}
}
if val, ok := urlMap["subcontainers"]; ok {
newBool, err := strconv.ParseBool(val[0])
if err == nil {
query.IncludeSubcontainers = newBool
}
}
if val, ok := urlMap["oom_events"]; ok {
newBool, err := strconv.ParseBool(val[0])
if err == nil {
query.EventType[events.TypeOom] = newBool
}
}
if val, ok := urlMap["creation_events"]; ok {
newBool, err := strconv.ParseBool(val[0])
if err == nil {
query.EventType[events.TypeContainerCreation] = newBool
}
}
if val, ok := urlMap["deletion_events"]; ok {
newBool, err := strconv.ParseBool(val[0])
if err == nil {
query.EventType[events.TypeContainerDeletion] = newBool
}
}
if val, ok := urlMap["max_events"]; ok {
newInt, err := strconv.Atoi(val[0])
if err == nil {
query.MaxEventsReturned = int(newInt)
}
}
if val, ok := urlMap["start_time"]; ok {
newTime, err := time.Parse(time.RFC3339, val[0])
if err == nil {
query.StartTime = newTime
}
}
if val, ok := urlMap["end_time"]; ok {
newTime, err := time.Parse(time.RFC3339, val[0])
if err == nil {
query.EndTime = newTime
}
}
glog.V(2).Infof(
"%v was returned in api/handler.go:getEventRequest from the url rawQuery %v",
query, r.URL.RawQuery)
return query, getHistoricalEvents, nil
}
func getContainerName(request []string) string {
return path.Join("/", strings.Join(request, "/"))
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 api
import (
"fmt"
"net/http"
"github.com/golang/glog"
"github.com/google/cadvisor/events"
info "github.com/google/cadvisor/info/v1"
"github.com/google/cadvisor/info/v2"
"github.com/google/cadvisor/manager"
)
const (
containersApi = "containers"
subcontainersApi = "subcontainers"
machineApi = "machine"
dockerApi = "docker"
summaryApi = "summary"
specApi = "spec"
eventsApi = "events"
)
// Interface for a cAdvisor API version
type ApiVersion interface {
// Returns the version string.
Version() string
// List of supported API endpoints.
SupportedRequestTypes() []string
// Handles a request. The second argument is the parameters after /api/<version>/<endpoint>
HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error
}
// Gets all supported API versions.
func getApiVersions() []ApiVersion {
v1_0 := &version1_0{}
v1_1 := newVersion1_1(v1_0)
v1_2 := newVersion1_2(v1_1)
v1_3 := newVersion1_3(v1_2)
v2_0 := newVersion2_0(v1_3)
return []ApiVersion{v1_0, v1_1, v1_2, v1_3, v2_0}
}
// API v1.0
type version1_0 struct {
}
func (self *version1_0) Version() string {
return "v1.0"
}
func (self *version1_0) SupportedRequestTypes() []string {
return []string{containersApi, machineApi}
}
func (self *version1_0) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
switch requestType {
case machineApi:
glog.V(2).Infof("Api - Machine")
// Get the MachineInfo
machineInfo, err := m.GetMachineInfo()
if err != nil {
return err
}
err = writeResult(machineInfo, w)
if err != nil {
return err
}
case containersApi:
containerName := getContainerName(request)
glog.V(2).Infof("Api - Container(%s)", containerName)
// Get the query request.
query, err := getContainerInfoRequest(r.Body)
if err != nil {
return err
}
// Get the container.
cont, err := m.GetContainerInfo(containerName, query)
if err != nil {
return fmt.Errorf("failed to get container %q with error: %s", containerName, err)
}
// Only output the container as JSON.
err = writeResult(cont, w)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown request type %q", requestType)
}
return nil
}
// API v1.1
type version1_1 struct {
baseVersion *version1_0
}
// v1.1 builds on v1.0.
func newVersion1_1(v *version1_0) *version1_1 {
return &version1_1{
baseVersion: v,
}
}
func (self *version1_1) Version() string {
return "v1.1"
}
func (self *version1_1) SupportedRequestTypes() []string {
return append(self.baseVersion.SupportedRequestTypes(), subcontainersApi)
}
func (self *version1_1) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
switch requestType {
case subcontainersApi:
containerName := getContainerName(request)
glog.V(2).Infof("Api - Subcontainers(%s)", containerName)
// Get the query request.
query, err := getContainerInfoRequest(r.Body)
if err != nil {
return err
}
// Get the subcontainers.
containers, err := m.SubcontainersInfo(containerName, query)
if err != nil {
return fmt.Errorf("failed to get subcontainers for container %q with error: %s", containerName, err)
}
// Only output the containers as JSON.
err = writeResult(containers, w)
if err != nil {
return err
}
return nil
default:
return self.baseVersion.HandleRequest(requestType, request, m, w, r)
}
}
// API v1.2
type version1_2 struct {
baseVersion *version1_1
}
// v1.2 builds on v1.1.
func newVersion1_2(v *version1_1) *version1_2 {
return &version1_2{
baseVersion: v,
}
}
func (self *version1_2) Version() string {
return "v1.2"
}
func (self *version1_2) SupportedRequestTypes() []string {
return append(self.baseVersion.SupportedRequestTypes(), dockerApi)
}
func (self *version1_2) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
switch requestType {
case dockerApi:
glog.V(2).Infof("Api - Docker(%v)", request)
// Get the query request.
query, err := getContainerInfoRequest(r.Body)
if err != nil {
return err
}
var containers map[string]info.ContainerInfo
// map requests for "docker/" to "docker"
if len(request) == 1 && len(request[0]) == 0 {
request = request[:0]
}
switch len(request) {
case 0:
// Get all Docker containers.
containers, err = m.AllDockerContainers(query)
if err != nil {
return fmt.Errorf("failed to get all Docker containers with error: %v", err)
}
case 1:
// Get one Docker container.
var cont info.ContainerInfo
cont, err = m.DockerContainer(request[0], query)
if err != nil {
return fmt.Errorf("failed to get Docker container %q with error: %v", request[0], err)
}
containers = map[string]info.ContainerInfo{
cont.Name: cont,
}
default:
return fmt.Errorf("unknown request for Docker container %v", request)
}
// Only output the containers as JSON.
err = writeResult(containers, w)
if err != nil {
return err
}
return nil
default:
return self.baseVersion.HandleRequest(requestType, request, m, w, r)
}
}
// API v1.3
type version1_3 struct {
baseVersion *version1_2
}
// v1.3 builds on v1.2.
func newVersion1_3(v *version1_2) *version1_3 {
return &version1_3{
baseVersion: v,
}
}
func (self *version1_3) Version() string {
return "v1.3"
}
func (self *version1_3) SupportedRequestTypes() []string {
return append(self.baseVersion.SupportedRequestTypes(), eventsApi)
}
func (self *version1_3) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
switch requestType {
case eventsApi:
query, eventsFromAllTime, err := getEventRequest(r)
if err != nil {
return err
}
glog.V(2).Infof("Api - Events(%v)", query)
if eventsFromAllTime {
pastEvents, err := m.GetPastEvents(query)
if err != nil {
return err
}
return writeResult(pastEvents, w)
}
eventsChannel := make(chan *events.Event, 10)
err = m.WatchForEvents(query, eventsChannel)
if err != nil {
return err
}
return streamResults(eventsChannel, w, r)
default:
return self.baseVersion.HandleRequest(requestType, request, m, w, r)
}
}
// API v2.0
// v2.0 builds on v1.3
type version2_0 struct {
baseVersion *version1_3
}
func newVersion2_0(v *version1_3) *version2_0 {
return &version2_0{
baseVersion: v,
}
}
func (self *version2_0) Version() string {
return "v2.0"
}
func (self *version2_0) SupportedRequestTypes() []string {
return append(self.baseVersion.SupportedRequestTypes(), summaryApi)
}
func (self *version2_0) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error {
switch requestType {
case summaryApi:
containerName := getContainerName(request)
glog.V(2).Infof("Api - Summary(%v)", containerName)
stats, err := m.GetContainerDerivedStats(containerName)
if err != nil {
return err
}
return writeResult(stats, w)
case specApi:
containerName := getContainerName(request)
glog.V(2).Infof("Api - Spec(%v)", containerName)
spec, err := m.GetContainerSpec(containerName)
if err != nil {
return err
}
specV2 := convertSpec(spec)
return writeResult(specV2, w)
default:
return self.baseVersion.HandleRequest(requestType, request, m, w, r)
}
}
// Convert container spec from v1 to v2.
func convertSpec(specV1 info.ContainerSpec) v2.ContainerSpec {
specV2 := v2.ContainerSpec{
CreationTime: specV1.CreationTime,
HasCpu: specV1.HasCpu,
HasMemory: specV1.HasMemory,
}
if specV1.HasCpu {
specV2.Cpu.Limit = specV1.Cpu.Limit
specV2.Cpu.MaxLimit = specV1.Cpu.MaxLimit
specV2.Cpu.Mask = specV1.Cpu.Mask
}
if specV1.HasMemory {
specV2.Memory.Limit = specV1.Memory.Limit
specV2.Memory.Reservation = specV1.Memory.Reservation
specV2.Memory.SwapLimit = specV1.Memory.SwapLimit
}
return specV2
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 api
import (
"io"
"net/http"
"reflect"
"testing"
"github.com/google/cadvisor/events"
"github.com/stretchr/testify/assert"
)
// returns an http.Request pointer for an input url test string
func makeHTTPRequest(requestURL string, t *testing.T) *http.Request {
dummyReader, _ := io.Pipe()
r, err := http.NewRequest("GET", requestURL, dummyReader)
assert.Nil(t, err)
return r
}
func TestGetEventRequestBasicRequest(t *testing.T) {
r := makeHTTPRequest("http://localhost:8080/api/v1.3/events?oom_events=true&historical=true&max_events=10", t)
expectedQuery := &events.Request{
EventType: map[events.EventType]bool{
events.TypeOom: true,
},
MaxEventsReturned: 10,
}
receivedQuery, getHistoricalEvents, err := getEventRequest(r)
if !reflect.DeepEqual(expectedQuery, receivedQuery) {
t.Errorf("expected %v but received %v", expectedQuery, receivedQuery)
}
assert.True(t, getHistoricalEvents)
assert.Nil(t, err)
}
func TestGetEventEmptyRequest(t *testing.T) {
r := makeHTTPRequest("", t)
expectedQuery := events.NewRequest()
receivedQuery, getHistoricalEvents, err := getEventRequest(r)
if !reflect.DeepEqual(expectedQuery, receivedQuery) {
t.Errorf("expected %v but received %v", expectedQuery, receivedQuery)
}
assert.False(t, getHistoricalEvents)
assert.Nil(t, err)
}
func TestGetEventRequestDoubleArgument(t *testing.T) {
r := makeHTTPRequest("http://localhost:8080/api/v1.3/events?historical=true&oom_events=true&oom_events=false", t)
expectedQuery := &events.Request{
EventType: map[events.EventType]bool{
events.TypeOom: true,
},
}
receivedQuery, getHistoricalEvents, err := getEventRequest(r)
if !reflect.DeepEqual(expectedQuery, receivedQuery) {
t.Errorf("expected %v but received %v", expectedQuery, receivedQuery)
}
assert.True(t, getHistoricalEvents)
assert.Nil(t, err)
}
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 healthz
import (
"net/http"
httpMux "github.com/google/cadvisor/http/mux"
)
func handleHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
// Register simple HTTP /healthz handler to return "ok".
func RegisterHandler(mux httpMux.Mux) error {
mux.HandleFunc("/healthz", handleHealthz)
return nil
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 http
import (
"fmt"
"net/http"
auth "github.com/abbot/go-http-auth"
"github.com/golang/glog"
"github.com/google/cadvisor/api"
"github.com/google/cadvisor/healthz"
httpMux "github.com/google/cadvisor/http/mux"
"github.com/google/cadvisor/manager"
"github.com/google/cadvisor/pages"
"github.com/google/cadvisor/pages/static"
"github.com/google/cadvisor/validate"
)
func RegisterHandlers(mux httpMux.Mux, containerManager manager.Manager, httpAuthFile, httpAuthRealm, httpDigestFile, httpDigestRealm string) error {
// Basic health handler.
if err := healthz.RegisterHandler(mux); err != nil {
return fmt.Errorf("failed to register healthz handler: %s", err)
}
// Validation/Debug handler.
mux.HandleFunc(validate.ValidatePage, func(w http.ResponseWriter, r *http.Request) {
err := validate.HandleRequest(w, containerManager)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
})
// Register API handler.
if err := api.RegisterHandlers(mux, containerManager); err != nil {
return fmt.Errorf("failed to register API handlers: %s", err)
}
// Redirect / to containers page.
mux.Handle("/", http.RedirectHandler(pages.ContainersPage, http.StatusTemporaryRedirect))
var authenticated bool = false
// Setup the authenticator object
if httpAuthFile != "" {
glog.Infof("Using auth file %s", httpAuthFile)
secrets := auth.HtpasswdFileProvider(httpAuthFile)
authenticator := auth.NewBasicAuthenticator(httpAuthRealm, secrets)
mux.HandleFunc(static.StaticResource, authenticator.Wrap(staticHandler))
if err := pages.RegisterHandlersBasic(mux, containerManager, authenticator); err != nil {
return fmt.Errorf("failed to register pages auth handlers: %s", err)
}
authenticated = true
}
if httpAuthFile == "" && httpDigestFile != "" {
glog.Infof("Using digest file %s", httpDigestFile)
secrets := auth.HtdigestFileProvider(httpDigestFile)
authenticator := auth.NewDigestAuthenticator(httpDigestRealm, secrets)
mux.HandleFunc(static.StaticResource, authenticator.Wrap(staticHandler))
if err := pages.RegisterHandlersDigest(mux, containerManager, authenticator); err != nil {
fmt.Errorf("failed to register pages digest handlers: %s", err)
}
authenticated = true
}
// Change handler based on authenticator initalization
if !authenticated {
mux.HandleFunc(static.StaticResource, staticHandlerNoAuth)
if err := pages.RegisterHandlersBasic(mux, containerManager, nil); err != nil {
return fmt.Errorf("failed to register pages handlers: %s", err)
}
}
return nil
}
func staticHandlerNoAuth(w http.ResponseWriter, r *http.Request) {
err := static.HandleRequest(w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
func staticHandler(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
err := static.HandleRequest(w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 mux
import (
"net/http"
)
// Mux interface expected by cAdvisor components.
type Mux interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
Handler(r *http.Request) (http.Handler, string)
Handle(pattern string, handler http.Handler)
}
...@@ -127,7 +127,7 @@ func New(memoryStorage *memory.InMemoryStorage, sysfs sysfs.SysFs) (Manager, err ...@@ -127,7 +127,7 @@ func New(memoryStorage *memory.InMemoryStorage, sysfs sysfs.SysFs) (Manager, err
// Register the raw driver. // Register the raw driver.
err = raw.Register(newManager) err = raw.Register(newManager)
if err != nil { if err != nil {
return nil, fmt.Errorf("registration of the raw container factory failed: %v", err) glog.Errorf("Registration of the raw container factory failed: %v", err)
} }
return newManager, nil return newManager, nil
......
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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.
// Page for /containers/
package pages
import (
"fmt"
"html/template"
"math"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/golang/glog"
info "github.com/google/cadvisor/info/v1"
"github.com/google/cadvisor/manager"
)
const ContainersPage = "/containers/"
// from http://golang.org/doc/effective_go.html#constants
type ByteSize float64
const (
_ = iota
// KB - kilobyte
KB ByteSize = 1 << (10 * iota)
// MB - megabyte
MB
// GB - gigabyte
GB
// TB - terabyte
TB
// PB - petabyte
PB
// EB - exabyte
EB
// ZB - zettabyte
ZB
// YB - yottabyte
YB
)
func (b ByteSize) Size() string {
for _, i := range [...]ByteSize{YB, ZB, EB, PB, TB, GB, MB, KB} {
if b >= i {
return fmt.Sprintf("%.2f", b/i)
}
}
return fmt.Sprintf("%.2f", b)
}
func (b ByteSize) Unit() string {
switch {
case b >= YB:
return "YB"
case b >= ZB:
return "ZB"
case b >= EB:
return "EB"
case b >= PB:
return "PB"
case b >= TB:
return "TB"
case b >= GB:
return "GB"
case b >= MB:
return "MB"
case b >= KB:
return "KB"
}
return "B"
}
var funcMap = template.FuncMap{
"printMask": printMask,
"printCores": printCores,
"printShares": printShares,
"printSize": printSize,
"printUnit": printUnit,
}
func printMask(mask string, numCores int) interface{} {
masks := make([]string, numCores)
activeCores := getActiveCores(mask)
for i := 0; i < numCores; i++ {
coreClass := "inactive-cpu"
if activeCores[i] {
coreClass = "active-cpu"
}
masks[i] = fmt.Sprintf("<span class=\"%s\">%d</span>", coreClass, i)
}
return template.HTML(strings.Join(masks, "&nbsp;"))
}
func getActiveCores(mask string) map[int]bool {
activeCores := make(map[int]bool)
for _, corebits := range strings.Split(mask, ",") {
cores := strings.Split(corebits, "-")
if len(cores) == 1 {
index, err := strconv.Atoi(cores[0])
if err != nil {
// Ignore malformed strings.
continue
}
activeCores[index] = true
} else if len(cores) == 2 {
start, err := strconv.Atoi(cores[0])
if err != nil {
continue
}
end, err := strconv.Atoi(cores[1])
if err != nil {
continue
}
for i := start; i <= end; i++ {
activeCores[i] = true
}
}
}
return activeCores
}
func printCores(millicores *uint64) string {
cores := float64(*millicores) / 1000
return strconv.FormatFloat(cores, 'f', 3, 64)
}
func printShares(shares *uint64) string {
return fmt.Sprintf("%d", *shares)
}
func toMegabytes(bytes uint64) float64 {
return float64(bytes) / (1 << 20)
}
func printSize(bytes uint64) string {
if bytes >= math.MaxInt64 {
return "unlimited"
}
return ByteSize(bytes).Size()
}
func printUnit(bytes uint64) string {
if bytes >= math.MaxInt64 {
return ""
}
return ByteSize(bytes).Unit()
}
func toMemoryPercent(usage uint64, spec *info.ContainerSpec, machine *info.MachineInfo) int {
// Saturate limit to the machine size.
limit := uint64(spec.Memory.Limit)
if limit > uint64(machine.MemoryCapacity) {
limit = uint64(machine.MemoryCapacity)
}
return int((usage * 100) / limit)
}
func serveContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
start := time.Now()
// The container name is the path after the handler
containerName := u.Path[len(ContainersPage)-1:]
// Get the container.
reqParams := info.ContainerInfoRequest{
NumStats: 60,
}
cont, err := m.GetContainerInfo(containerName, &reqParams)
if err != nil {
return fmt.Errorf("failed to get container %q with error: %v", containerName, err)
}
displayName := getContainerDisplayName(cont.ContainerReference)
// Get the MachineInfo
machineInfo, err := m.GetMachineInfo()
if err != nil {
return err
}
// Make a list of the parent containers and their links
pathParts := strings.Split(string(cont.Name), "/")
parentContainers := make([]link, 0, len(pathParts))
parentContainers = append(parentContainers, link{
Text: "root",
Link: ContainersPage,
})
for i := 1; i < len(pathParts); i++ {
// Skip empty parts.
if pathParts[i] == "" {
continue
}
parentContainers = append(parentContainers, link{
Text: pathParts[i],
Link: path.Join(ContainersPage, path.Join(pathParts[1:i+1]...)),
})
}
// Build the links for the subcontainers.
subcontainerLinks := make([]link, 0, len(cont.Subcontainers))
for _, sub := range cont.Subcontainers {
subcontainerLinks = append(subcontainerLinks, link{
Text: getContainerDisplayName(sub),
Link: path.Join(ContainersPage, sub.Name),
})
}
data := &pageData{
DisplayName: displayName,
ContainerName: cont.Name,
ParentContainers: parentContainers,
Subcontainers: subcontainerLinks,
Spec: cont.Spec,
Stats: cont.Stats,
MachineInfo: machineInfo,
IsRoot: cont.Name == "/",
ResourcesAvailable: cont.Spec.HasCpu || cont.Spec.HasMemory || cont.Spec.HasNetwork || cont.Spec.HasFilesystem,
CpuAvailable: cont.Spec.HasCpu,
MemoryAvailable: cont.Spec.HasMemory,
NetworkAvailable: cont.Spec.HasNetwork,
FsAvailable: cont.Spec.HasFilesystem,
}
err = pageTemplate.Execute(w, data)
if err != nil {
glog.Errorf("Failed to apply template: %s", err)
}
glog.V(1).Infof("Request took %s", time.Since(start))
return nil
}
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 pages
const containersHtmlTemplate = `
<html>
<head>
<title>cAdvisor - {{.DisplayName}}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/static/bootstrap-3.1.1.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="/static/bootstrap-theme-3.1.1.min.css">
<link rel="stylesheet" href="/static/containers.css">
<!-- Latest compiled and minified JavaScript -->
<script src="/static/jquery-1.10.2.min.js"></script>
<script src="/static/bootstrap-3.1.1.min.js"></script>
<script type="text/javascript" src="/static/google-jsapi.js"></script>
<script type="text/javascript" src="/static/containers.js"></script>
</head>
<body>
<div class="container theme-showcase" >
<div class="col-sm-12" id="logo">
</div>
<div class="col-sm-12">
<div class="page-header">
<h1>{{.DisplayName}}</h1>
</div>
<ol class="breadcrumb">
{{range $parentContainer := .ParentContainers}}
<li><a href="{{$parentContainer.Link}}">{{$parentContainer.Text}}</a></li>
{{end}}
</ol>
</div>
{{if .IsRoot}}
<div class="col-sm-12">
<h4><a href="/docker">Docker Containers</a></h4>
</div>
{{end}}
{{if .Subcontainers}}
<div class="col-sm-12">
<div class="page-header">
<h3>Subcontainers</h3>
</div>
<div class="list-group">
{{range $subcontainer := .Subcontainers}}
<a href="{{$subcontainer.Link}}" class="list-group-item">{{$subcontainer.Text}}</a>
{{end}}
</div>
</div>
{{end}}
{{if .ResourcesAvailable}}
<div class="col-sm-12">
<div class="page-header">
<h3>Isolation</h3>
</div>
{{if .CpuAvailable}}
<ul class="list-group">
<li class="list-group-item active isolation-title panel-title">CPU</li>
{{if .Spec.Cpu.Limit}}
<li class="list-group-item"><span class="stat-label">Shares</span> {{printShares .Spec.Cpu.Limit}} <span class="unit-label">shares</span></li>
{{end}}
{{if .Spec.Cpu.MaxLimit}}
<li class="list-group-item"><span class="stat-label">Max Limit</span> {{printCores .Spec.Cpu.MaxLimit}} <span class="unit-label">cores</span></li>
{{end}}
{{if .Spec.Cpu.Mask}}
<li class="list-group-item"><span class="stat-label">Allowed Cores</span> {{printMask .Spec.Cpu.Mask .MachineInfo.NumCores}}</li>
{{end}}
</ul>
{{end}}
{{if .MemoryAvailable}}
<ul class="list-group">
<li class="list-group-item active isolation-title panel-title">Memory</li>
{{if .Spec.Memory.Reservation}}
<li class="list-group-item"><span class="stat-label">Reservation</span> {{printSize .Spec.Memory.Reservation}} <span class="unit-label">{{printUnit .Spec.Memory.Reservation}}</span></li>
{{end}}
{{if .Spec.Memory.Limit}}
<li class="list-group-item"><span class="stat-label">Limit</span> {{printSize .Spec.Memory.Limit}} <span class="unit-label">{{printUnit .Spec.Memory.Limit}}</span></li>
{{end}}
{{if .Spec.Memory.SwapLimit}}
<li class="list-group-item"><span class="stat-label">Swap Limit</span> {{printSize .Spec.Memory.SwapLimit}} <span class="unit-label">{{printUnit .Spec.Memory.SwapLimit}}</span></li>
{{end}}
</ul>
{{end}}
</div>
<div class="col-sm-12">
<div class="page-header">
<h3>Usage</h3>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Overview</h3>
</div>
<div id="usage-gauge" class="panel-body"></div>
</div>
{{if .CpuAvailable}}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">CPU</h3>
</div>
<div class="panel-body">
<h4>Total Usage</h4>
<div id="cpu-total-usage-chart"></div>
<!-- <h4>CPU Load Average</h4>
<div id="cpu-load-chart"></div> -->
<h4>Usage per Core</h4>
<div id="cpu-per-core-usage-chart"></div>
<h4>Usage Breakdown</h4>
<div id="cpu-usage-breakdown-chart"></div>
</div>
</div>
{{end}}
{{if .MemoryAvailable}}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Memory</h3>
</div>
<div class="panel-body">
<h4>Total Usage</h4>
<div id="memory-usage-chart"></div>
<br/>
<div class="row col-sm-12">
<h4>Usage Breakdown</h4>
<div class="col-sm-9">
<div class="progress">
<div class="progress-bar progress-bar-danger" id="progress-hot-memory">
<span class="sr-only">Hot Memory</span>
</div>
<div class="progress-bar progress-bar-info" id="progress-cold-memory">
<span class="sr-only">Cold Memory</span>
</div>
</div>
</div>
<div class="col-sm-3" id="memory-text"></div>
</div>
</div>
</div>
{{end}}
{{if .NetworkAvailable}}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Network</h3>
</div>
<div class="panel-body">
<h4>Throughput</h4>
<div id="network-bytes-chart"></div>
</div>
<div class="panel-body">
<h4>Errors</h4>
<div id="network-errors-chart"></div>
</div>
</div>
{{end}}
{{if .FsAvailable}}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Filesystem</h3>
</div>
<div id="filesystem-usage" class="panel-body">
</div>
</div>
{{end}}
</div>
{{end}}
</div>
<script type="text/javascript">
startPage({{.ContainerName}}, {{.CpuAvailable}}, {{.MemoryAvailable}});
</script>
</body>
</html>
`
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 pages
import (
"fmt"
"net/http"
"net/url"
"path"
"time"
"github.com/golang/glog"
"github.com/google/cadvisor/container/docker"
info "github.com/google/cadvisor/info/v1"
"github.com/google/cadvisor/manager"
)
const DockerPage = "/docker/"
func serveDockerPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
start := time.Now()
// The container name is the path after the handler
containerName := u.Path[len(DockerPage):]
var data *pageData
if containerName == "" {
// Get the containers.
reqParams := info.ContainerInfoRequest{
NumStats: 0,
}
conts, err := m.AllDockerContainers(&reqParams)
if err != nil {
return fmt.Errorf("failed to get container %q with error: %v", containerName, err)
}
subcontainers := make([]link, 0, len(conts))
for _, cont := range conts {
subcontainers = append(subcontainers, link{
Text: getContainerDisplayName(cont.ContainerReference),
Link: path.Join("/docker", docker.ContainerNameToDockerId(cont.ContainerReference.Name)),
})
}
dockerContainersText := "Docker Containers"
data = &pageData{
DisplayName: dockerContainersText,
ParentContainers: []link{
{
Text: dockerContainersText,
Link: DockerPage,
}},
Subcontainers: subcontainers,
}
} else {
// Get the container.
reqParams := info.ContainerInfoRequest{
NumStats: 60,
}
cont, err := m.DockerContainer(containerName, &reqParams)
if err != nil {
return fmt.Errorf("failed to get container %q with error: %v", containerName, err)
}
displayName := getContainerDisplayName(cont.ContainerReference)
// Make a list of the parent containers and their links
var parentContainers []link
parentContainers = append(parentContainers, link{
Text: "Docker containers",
Link: DockerPage,
})
parentContainers = append(parentContainers, link{
Text: displayName,
Link: path.Join(DockerPage, docker.ContainerNameToDockerId(cont.Name)),
})
// Get the MachineInfo
machineInfo, err := m.GetMachineInfo()
if err != nil {
return err
}
data = &pageData{
DisplayName: displayName,
ContainerName: cont.Name,
ParentContainers: parentContainers,
Spec: cont.Spec,
Stats: cont.Stats,
MachineInfo: machineInfo,
ResourcesAvailable: cont.Spec.HasCpu || cont.Spec.HasMemory || cont.Spec.HasNetwork,
CpuAvailable: cont.Spec.HasCpu,
MemoryAvailable: cont.Spec.HasMemory,
NetworkAvailable: cont.Spec.HasNetwork,
FsAvailable: cont.Spec.HasFilesystem,
}
}
err := pageTemplate.Execute(w, data)
if err != nil {
glog.Errorf("Failed to apply template: %s", err)
}
glog.V(1).Infof("Request took %s", time.Since(start))
return nil
}
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 pages
import (
"fmt"
"html/template"
"net/http"
"strings"
auth "github.com/abbot/go-http-auth"
"github.com/golang/glog"
httpMux "github.com/google/cadvisor/http/mux"
info "github.com/google/cadvisor/info/v1"
"github.com/google/cadvisor/manager"
)
var pageTemplate *template.Template
type link struct {
// Text to show in the link.
Text string
// Web address to link to.
Link string
}
type pageData struct {
DisplayName string
ContainerName string
ParentContainers []link
Subcontainers []link
Spec info.ContainerSpec
Stats []*info.ContainerStats
MachineInfo *info.MachineInfo
IsRoot bool
ResourcesAvailable bool
CpuAvailable bool
MemoryAvailable bool
NetworkAvailable bool
FsAvailable bool
}
func init() {
pageTemplate = template.New("containersTemplate").Funcs(funcMap)
_, err := pageTemplate.Parse(containersHtmlTemplate)
if err != nil {
glog.Fatalf("Failed to parse template: %s", err)
}
}
func containerHandlerNoAuth(containerManager manager.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := serveContainersPage(containerManager, w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
}
func containerHandler(containerManager manager.Manager) auth.AuthenticatedHandlerFunc {
return func(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
err := serveContainersPage(containerManager, w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
}
func dockerHandlerNoAuth(containerManager manager.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := serveDockerPage(containerManager, w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
}
func dockerHandler(containerManager manager.Manager) auth.AuthenticatedHandlerFunc {
return func(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
err := serveDockerPage(containerManager, w, r.URL)
if err != nil {
fmt.Fprintf(w, "%s", err)
}
}
}
// Register http handlers
func RegisterHandlersDigest(mux httpMux.Mux, containerManager manager.Manager, authenticator *auth.DigestAuth) error {
// Register the handler for the containers page.
if authenticator != nil {
mux.HandleFunc(ContainersPage, authenticator.Wrap(containerHandler(containerManager)))
mux.HandleFunc(DockerPage, authenticator.Wrap(dockerHandler(containerManager)))
} else {
mux.HandleFunc(ContainersPage, containerHandlerNoAuth(containerManager))
mux.HandleFunc(DockerPage, dockerHandlerNoAuth(containerManager))
}
return nil
}
func RegisterHandlersBasic(mux httpMux.Mux, containerManager manager.Manager, authenticator *auth.BasicAuth) error {
// Register the handler for the containers and docker age.
if authenticator != nil {
mux.HandleFunc(ContainersPage, authenticator.Wrap(containerHandler(containerManager)))
mux.HandleFunc(DockerPage, authenticator.Wrap(dockerHandler(containerManager)))
} else {
mux.HandleFunc(ContainersPage, containerHandlerNoAuth(containerManager))
mux.HandleFunc(DockerPage, dockerHandlerNoAuth(containerManager))
}
return nil
}
func getContainerDisplayName(cont info.ContainerReference) string {
// Pick a user-added alias as display name.
displayName := ""
for _, alias := range cont.Aliases {
// ignore container id as alias.
if strings.Contains(cont.Name, alias) {
continue
}
// pick shortest display name if multiple aliases are available.
if displayName == "" || len(displayName) >= len(alias) {
displayName = alias
}
}
if displayName == "" {
displayName = cont.Name
} else if len(displayName) > 50 {
// truncate display name to fit in one line.
displayName = displayName[:50] + "..."
}
// Add the full container name to the display name.
if displayName != cont.Name {
displayName = fmt.Sprintf("%s (%s)", displayName, cont.Name)
}
return displayName
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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.
// Handler for /static content.
package static
import (
"fmt"
"net/http"
"net/url"
)
const StaticResource = "/static/"
var staticFiles = map[string]string{
"containers.css": containersCss,
"containers.js": containersJs,
"bootstrap-3.1.1.min.css": bootstrapCss,
"bootstrap-theme-3.1.1.min.css": bootstrapThemeCss,
"jquery-1.10.2.min.js": jqueryJs,
"bootstrap-3.1.1.min.js": bootstrapJs,
"google-jsapi.js": googleJsapiJs,
}
func HandleRequest(w http.ResponseWriter, u *url.URL) error {
if len(u.Path) <= len(StaticResource) {
return fmt.Errorf("unknown static resource %q", u.Path)
}
// Get the static content if it exists.
resource := u.Path[len(StaticResource):]
content, ok := staticFiles[resource]
if !ok {
return fmt.Errorf("unknown static resource %q", resource)
}
_, err := w.Write([]byte(content))
return err
}
package elb_test
import (
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/elb"
"github.com/mitchellh/goamz/testutil"
. "github.com/motain/gocheck"
"testing"
)
func Test(t *testing.T) {
TestingT(t)
}
type S struct {
elb *elb.ELB
}
var _ = Suite(&S{})
var testServer = testutil.NewHTTPServer()
func (s *S) SetUpSuite(c *C) {
testServer.Start()
auth := aws.Auth{"abc", "123", ""}
s.elb = elb.NewWithClient(auth, aws.Region{ELBEndpoint: testServer.URL}, testutil.DefaultClient)
}
func (s *S) TearDownTest(c *C) {
testServer.Flush()
}
func (s *S) TestAddTags(c *C) {
testServer.Response(200, nil, AddTagsExample)
options := elb.AddTags{
LoadBalancerNames: []string{"foobar"},
Tags: []elb.Tag{
{
Key: "hello",
Value: "world",
},
},
}
resp, err := s.elb.AddTags(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"AddTags"})
c.Assert(req.Form["LoadBalancerNames.member.1"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Tags.member.1.Key"], DeepEquals, []string{"hello"})
c.Assert(req.Form["Tags.member.1.Value"], DeepEquals, []string{"world"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "360e81f7-1100-11e4-b6ed-0f30EXAMPLE")
}
func (s *S) TestRemoveTags(c *C) {
testServer.Response(200, nil, RemoveTagsExample)
options := elb.RemoveTags{
LoadBalancerNames: []string{"foobar"},
TagKeys: []string{"hello"},
}
resp, err := s.elb.RemoveTags(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"RemoveTags"})
c.Assert(req.Form["LoadBalancerNames.member.1"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Tags.member.1.Key"], DeepEquals, []string{"hello"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestCreateLoadBalancer(c *C) {
testServer.Response(200, nil, CreateLoadBalancerExample)
options := elb.CreateLoadBalancer{
AvailZone: []string{"us-east-1a"},
Listeners: []elb.Listener{elb.Listener{
InstancePort: 80,
InstanceProtocol: "http",
SSLCertificateId: "needToAddASSLCertToYourAWSAccount",
LoadBalancerPort: 80,
Protocol: "http",
},
},
LoadBalancerName: "foobar",
Internal: false,
SecurityGroups: []string{"sg1"},
Subnets: []string{"sn1"},
}
resp, err := s.elb.CreateLoadBalancer(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateLoadBalancer"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "1549581b-12b7-11e3-895e-1334aEXAMPLE")
}
func (s *S) TestDeleteLoadBalancer(c *C) {
testServer.Response(200, nil, DeleteLoadBalancerExample)
options := elb.DeleteLoadBalancer{
LoadBalancerName: "foobar",
}
resp, err := s.elb.DeleteLoadBalancer(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteLoadBalancer"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "1549581b-12b7-11e3-895e-1334aEXAMPLE")
}
func (s *S) TestDescribeLoadBalancers(c *C) {
testServer.Response(200, nil, DescribeLoadBalancersExample)
options := elb.DescribeLoadBalancer{
Names: []string{"foobar"},
}
resp, err := s.elb.DescribeLoadBalancers(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeLoadBalancers"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
c.Assert(resp.LoadBalancers[0].LoadBalancerName, Equals, "MyLoadBalancer")
c.Assert(resp.LoadBalancers[0].Listeners[0].Protocol, Equals, "HTTP")
c.Assert(resp.LoadBalancers[0].Instances[0].InstanceId, Equals, "i-e4cbe38d")
c.Assert(resp.LoadBalancers[0].AvailabilityZones[0].AvailabilityZone, Equals, "us-east-1a")
c.Assert(resp.LoadBalancers[0].Scheme, Equals, "internet-facing")
c.Assert(resp.LoadBalancers[0].DNSName, Equals, "MyLoadBalancer-123456789.us-east-1.elb.amazonaws.com")
c.Assert(resp.LoadBalancers[0].HealthCheck.HealthyThreshold, Equals, int64(2))
c.Assert(resp.LoadBalancers[0].HealthCheck.UnhealthyThreshold, Equals, int64(10))
c.Assert(resp.LoadBalancers[0].HealthCheck.Interval, Equals, int64(90))
c.Assert(resp.LoadBalancers[0].HealthCheck.Target, Equals, "HTTP:80/")
c.Assert(resp.LoadBalancers[0].HealthCheck.Timeout, Equals, int64(60))
}
func (s *S) TestRegisterInstancesWithLoadBalancer(c *C) {
testServer.Response(200, nil, RegisterInstancesWithLoadBalancerExample)
options := elb.RegisterInstancesWithLoadBalancer{
LoadBalancerName: "foobar",
Instances: []string{"instance-1", "instance-2"},
}
resp, err := s.elb.RegisterInstancesWithLoadBalancer(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"RegisterInstancesWithLoadBalancer"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Instances.member.1.InstanceId"], DeepEquals, []string{"instance-1"})
c.Assert(req.Form["Instances.member.2.InstanceId"], DeepEquals, []string{"instance-2"})
c.Assert(err, IsNil)
c.Assert(resp.Instances[0].InstanceId, Equals, "i-315b7e51")
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestDeregisterInstancesFromLoadBalancer(c *C) {
testServer.Response(200, nil, DeregisterInstancesFromLoadBalancerExample)
options := elb.DeregisterInstancesFromLoadBalancer{
LoadBalancerName: "foobar",
Instances: []string{"instance-1", "instance-2"},
}
resp, err := s.elb.DeregisterInstancesFromLoadBalancer(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DeregisterInstancesFromLoadBalancer"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Instances.member.1.InstanceId"], DeepEquals, []string{"instance-1"})
c.Assert(req.Form["Instances.member.2.InstanceId"], DeepEquals, []string{"instance-2"})
c.Assert(err, IsNil)
c.Assert(resp.Instances[0].InstanceId, Equals, "i-6ec63d59")
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestConfigureHealthCheck(c *C) {
testServer.Response(200, nil, ConfigureHealthCheckExample)
options := elb.ConfigureHealthCheck{
LoadBalancerName: "foobar",
Check: elb.HealthCheck{
HealthyThreshold: 2,
UnhealthyThreshold: 2,
Interval: 30,
Target: "HTTP:80/ping",
Timeout: 3,
},
}
resp, err := s.elb.ConfigureHealthCheck(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"ConfigureHealthCheck"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(err, IsNil)
c.Assert(resp.Check.HealthyThreshold, Equals, int64(2))
c.Assert(resp.Check.UnhealthyThreshold, Equals, int64(2))
c.Assert(resp.Check.Interval, Equals, int64(30))
c.Assert(resp.Check.Target, Equals, "HTTP:80/ping")
c.Assert(resp.Check.Timeout, Equals, int64(3))
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestDescribeInstanceHealth(c *C) {
testServer.Response(200, nil, DescribeInstanceHealthExample)
options := elb.DescribeInstanceHealth{
LoadBalancerName: "foobar",
}
resp, err := s.elb.DescribeInstanceHealth(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstanceHealth"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(err, IsNil)
c.Assert(resp.InstanceStates[0].InstanceId, Equals, "i-90d8c2a5")
c.Assert(resp.InstanceStates[0].State, Equals, "InService")
c.Assert(resp.InstanceStates[1].InstanceId, Equals, "i-06ea3e60")
c.Assert(resp.InstanceStates[1].State, Equals, "OutOfService")
c.Assert(resp.RequestId, Equals, "1549581b-12b7-11e3-895e-1334aEXAMPLE")
}
package elb_test
var ErrorDump = `
<?xml version="1.0" encoding="UTF-8"?>
<Response><Errors><Error><Code>UnsupportedOperation</Code>
<Message></Message>
</Error></Errors><RequestID>0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4</RequestID></Response>
`
// http://goo.gl/OkMdtJ
var AddTagsExample = `
<AddTagsResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<AddTagsResult/>
<ResponseMetadata>
<RequestId>360e81f7-1100-11e4-b6ed-0f30EXAMPLE</RequestId>
</ResponseMetadata>
</AddTagsResponse>
`
// http://goo.gl/nT2E89
var RemoveTagsExample = `
<RemoveTagsResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<RemoveTagsResult/>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</RemoveTagsResponse>
`
// http://goo.gl/gQRD2H
var CreateLoadBalancerExample = `
<CreateLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<CreateLoadBalancerResult>
<DNSName>MyLoadBalancer-1234567890.us-east-1.elb.amazonaws.com</DNSName>
</CreateLoadBalancerResult>
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
</CreateLoadBalancerResponse>
`
// http://goo.gl/GLZeBN
var DeleteLoadBalancerExample = `
<DeleteLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
</DeleteLoadBalancerResponse>
`
// http://goo.gl/8UgpQ8
var DescribeLoadBalancersExample = `
<DescribeLoadBalancersResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DescribeLoadBalancersResult>
<LoadBalancerDescriptions>
<member>
<SecurityGroups/>
<LoadBalancerName>MyLoadBalancer</LoadBalancerName>
<CreatedTime>2013-05-24T21:15:31.280Z</CreatedTime>
<HealthCheck>
<Interval>90</Interval>
<Target>HTTP:80/</Target>
<HealthyThreshold>2</HealthyThreshold>
<Timeout>60</Timeout>
<UnhealthyThreshold>10</UnhealthyThreshold>
</HealthCheck>
<ListenerDescriptions>
<member>
<PolicyNames/>
<Listener>
<Protocol>HTTP</Protocol>
<LoadBalancerPort>80</LoadBalancerPort>
<InstanceProtocol>HTTP</InstanceProtocol>
<SSLCertificateId>needToAddASSLCertToYourAWSAccount</SSLCertificateId>
<InstancePort>80</InstancePort>
</Listener>
</member>
</ListenerDescriptions>
<Instances>
<member>
<InstanceId>i-e4cbe38d</InstanceId>
</member>
</Instances>
<Policies>
<AppCookieStickinessPolicies/>
<OtherPolicies/>
<LBCookieStickinessPolicies/>
</Policies>
<AvailabilityZones>
<member>us-east-1a</member>
</AvailabilityZones>
<CanonicalHostedZoneNameID>ZZZZZZZZZZZ123X</CanonicalHostedZoneNameID>
<CanonicalHostedZoneName>MyLoadBalancer-123456789.us-east-1.elb.amazonaws.com</CanonicalHostedZoneName>
<Scheme>internet-facing</Scheme>
<SourceSecurityGroup>
<OwnerAlias>amazon-elb</OwnerAlias>
<GroupName>amazon-elb-sg</GroupName>
</SourceSecurityGroup>
<DNSName>MyLoadBalancer-123456789.us-east-1.elb.amazonaws.com</DNSName>
<BackendServerDescriptions/>
<Subnets/>
</member>
</LoadBalancerDescriptions>
</DescribeLoadBalancersResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</DescribeLoadBalancersResponse>
`
// http://goo.gl/Uz1N66
var RegisterInstancesWithLoadBalancerExample = `
<RegisterInstancesWithLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<RegisterInstancesWithLoadBalancerResult>
<Instances>
<member>
<InstanceId>i-315b7e51</InstanceId>
</member>
</Instances>
</RegisterInstancesWithLoadBalancerResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</RegisterInstancesWithLoadBalancerResponse>
`
// http://goo.gl/5OMv62
var DeregisterInstancesFromLoadBalancerExample = `
<DeregisterInstancesFromLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DeregisterInstancesFromLoadBalancerResult>
<Instances>
<member>
<InstanceId>i-6ec63d59</InstanceId>
</member>
</Instances>
</DeregisterInstancesFromLoadBalancerResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</DeregisterInstancesFromLoadBalancerResponse>
`
// http://docs.aws.amazon.com/ElasticLoadBalancing/latest/APIReference/API_ConfigureHealthCheck.html
var ConfigureHealthCheckExample = `
<ConfigureHealthCheckResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<ConfigureHealthCheckResult>
<HealthCheck>
<Interval>30</Interval>
<Target>HTTP:80/ping</Target>
<HealthyThreshold>2</HealthyThreshold>
<Timeout>3</Timeout>
<UnhealthyThreshold>2</UnhealthyThreshold>
</HealthCheck>
</ConfigureHealthCheckResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</ConfigureHealthCheckResponse>`
// http://goo.gl/cGNxfj
var DescribeInstanceHealthExample = `
<DescribeInstanceHealthResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DescribeInstanceHealthResult>
<InstanceStates>
<member>
<Description>N/A</Description>
<InstanceId>i-90d8c2a5</InstanceId>
<State>InService</State>
<ReasonCode>N/A</ReasonCode>
</member>
<member>
<Description>N/A</Description>
<InstanceId>i-06ea3e60</InstanceId>
<State>OutOfService</State>
<ReasonCode>N/A</ReasonCode>
</member>
</InstanceStates>
</DescribeInstanceHealthResult>
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
</DescribeInstanceHealthResponse>`
package elb
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"github.com/mitchellh/goamz/aws"
"sort"
"strings"
)
// ----------------------------------------------------------------------------
// Version 2 signing (http://goo.gl/RSRp5)
var b64 = base64.StdEncoding
func sign(auth aws.Auth, method, path string, params map[string]string, host string) {
params["AWSAccessKeyId"] = auth.AccessKey
params["SignatureVersion"] = "2"
params["SignatureMethod"] = "HmacSHA256"
if auth.Token != "" {
params["SecurityToken"] = auth.Token
}
var sarray []string
for k, v := range params {
sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(v))
}
sort.StringSlice(sarray).Sort()
joined := strings.Join(sarray, "&")
payload := method + "\n" + host + "\n" + path + "\n" + joined
hash := hmac.New(sha256.New, []byte(auth.SecretKey))
hash.Write([]byte(payload))
signature := make([]byte, b64.EncodedLen(hash.Size()))
b64.Encode(signature, hash.Sum(nil))
params["Signature"] = string(signature)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment