VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

1
vendor/github.com/vmware/vic/cmd/vicadmin/html generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../isos/vicadmin

530
vendor/github.com/vmware/vic/cmd/vicadmin/logs.go generated vendored Normal file
View File

@@ -0,0 +1,530 @@
// Copyright 2016-2017 VMware, 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 main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"path"
"sort"
"strings"
"sync"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/hpcloud/tail"
"path/filepath"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/pprof"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
)
const (
nBytes = 1024
tailLines = 8
uint32max = (1 << 32) - 1
// how many lines of log data to collect
logLines = 5000
// how many lines to request per call
lines = 500
)
type dlogReader struct {
c *session.Session
name string
host *object.HostSystem
}
func (r dlogReader) open() (entry, error) {
defer trace.End(trace.Begin(r.name))
name := r.name
if r.host != nil {
name = fmt.Sprintf("%s-%s", path.Base(r.host.InventoryPath), r.name)
}
m := object.NewDiagnosticManager(r.c.Vim25())
ctx := context.Background()
// Currently we collect the tail of diagnostic log files to avoid
// reading the entire file into memory or writing local disk.
// get LineEnd without any LineText
h, err := m.BrowseLog(ctx, r.host, r.name, math.MaxInt32, 0)
if err != nil {
return nil, err
}
end := h.LineEnd
start := end - logLines
var buf bytes.Buffer
for start < end {
h, err = m.BrowseLog(ctx, r.host, r.name, start, lines)
if err != nil {
return nil, err
}
for _, line := range h.LineText {
buf.WriteString(line)
buf.WriteByte('\n')
}
start += lines
}
return newBytesEntry(name+".log", buf.Bytes()), nil
}
// logFiles has a potential race condition since logrotation can rotate files this moment of time.
// however, the likely hood of this race is so low that it doesn't worth investing the time to do
// cross process synchronization.
func logFiles() []string {
defer trace.End(trace.Begin(""))
files, err := ioutil.ReadDir(logFileDir)
if err != nil {
log.Errorf("Failed to get a list of log files: %s", err)
return nil
}
names := []string{}
for _, fileInfo := range files {
if fileInfo.IsDir() {
continue
}
fname := fileInfo.Name()
log.Debugf("Found potential file for export: %s", fname)
for _, f := range logFileListPrefixes {
if strings.HasPrefix(fname, f) {
fp := filepath.Join(logFileDir, fname)
log.Debugf("Adding file for export: %s", fp)
names = append(names, fp)
break
}
}
}
return names
}
// logFile writes the contents of file f and any of it's rotated files to the http writer.
// includeF is used for tailing, in which case we want to write rotated files but not f itself
func writeLogFiles(w http.ResponseWriter, r *http.Request, f string, includeF bool) {
defer trace.End(trace.Begin(""))
files, err := ioutil.ReadDir(logFileDir)
if err != nil {
log.Errorf("Failed to get a list of log files: %s", err)
return
}
// find all rotated files, but not f itself
names := []string{}
for _, fileInfo := range files {
fname := fileInfo.Name()
if fileInfo.IsDir() || (fname == f) {
continue
}
log.Debugf("Found potential file for export: %s", fname)
if strings.HasPrefix(fname, f) {
fp := filepath.Join(logFileDir, fname)
names = append(names, fp)
}
}
// sort file names to preserve time rotation order
sort.Strings(names)
// find f last and append it to names
for _, fileInfo := range files {
fname := fileInfo.Name()
if fileInfo.IsDir() {
continue
}
if fname == f {
log.Debugf("Found potential file for export: %s", fname)
fp := filepath.Join(logFileDir, fname)
names = append(names, fp)
}
}
if len(names) == 0 {
http.NotFound(w, r)
}
// write file contents to w
for _, fileName := range names {
file, err := os.Open(fileName)
log.Debugf("Writing contents of: %s", fileName)
if err != nil {
log.Errorf("error opening file %s: %s", fileName, err.Error())
continue
}
// using interface type here so we can reassign r as a gzip reader for rotated logs
var r io.ReadCloser = file
if strings.HasSuffix(fileName, "gz") {
r, err = gzip.NewReader(file)
if err != nil {
log.Errorf("error opening gzipped file %s: %s", fileName, err.Error())
continue
}
}
_, err = io.Copy(w, r)
if err != nil {
log.Errorf("error writing contents of %s: %s", fileName, err.Error())
continue
}
r.Close()
file.Close()
}
}
func configureReaders() map[string]entryReader {
defer trace.End(trace.Begin(""))
pprofPaths := map[string]string{
// verbose
"verbose": "/debug/pprof/goroutine?debug=2",
// concise
"concise": "/debug/pprof/goroutine?debug=1",
"block": "/debug/pprof/block?debug=1",
"heap": "/debug/pprof/heap?debug=1",
"profile": "/debug/pprof/profile",
}
pprofSources := map[string]string{
"docker": pprof.GetPprofEndpoint(pprof.DockerPort).String(),
"portlayer": pprof.GetPprofEndpoint(pprof.PortlayerPort).String(),
"vicadm": pprof.GetPprofEndpoint(pprof.VicadminPort).String(),
"vic-init": pprof.GetPprofEndpoint(pprof.VCHInitPort).String(),
}
readers := map[string]entryReader{
"proc-mounts": fileReader("/proc/mounts"),
"uptime": commandReader("uptime"),
"df": commandReader("df"),
"free": commandReader("free"),
"netstat": commandReader("netstat -ant"),
"iptables": commandReader("sudo iptables --list"),
"ip-link": commandReader("ip link"),
"ip-addr": commandReader("ip addr"),
"ip-route": commandReader("ip route"),
"lsmod": commandReader("lsmod"),
// TODO: ls without shelling out
"disk-by-path": commandReader("ls -l /dev/disk/by-path"),
"disk-by-label": commandReader("ls -l /dev/disk/by-label"),
"disk-by-uuid": commandReader("ls -l /dev/disk/by-uuid"),
"lsblk": commandReader("lsblk -S"),
// To check we are not leaking any fds
"proc-self-fd": commandReader("ls -l /proc/self/fd"),
"ps": commandReader("ps -ef"),
"meminfo": fileReader("/proc/meminfo"),
"journalctl": commandReader("/bin/journalctl --no-pager"),
"dmesg": commandReader("/bin/journalctl --dmesg --no-pager"),
"sys-block": commandReader("ls -l /sys/block/"),
// To check version
"VERSION": versionReader("version"),
}
// add the pprof collection
for sname, source := range pprofSources {
for pname, paths := range pprofPaths {
rname := fmt.Sprintf("%s/%s", sname, pname)
readers[rname] = urlReader(source + paths)
}
}
for _, path := range logFiles() {
// Strip off leading '/'
readers[path[1:]] = fileReader(path)
}
return readers
}
func findDiagnosticLogs(c *session.Session) (map[string]entryReader, error) {
defer trace.End(trace.Begin(""))
// When connected to VC, we collect vpxd.log and hostd.log for all cluster hosts attached to the datastore.
// When connected to ESX, we just collect hostd.log.
const (
vpxdKey = "vpxd:vpxd.log"
hostdKey = "hostd"
)
logs := map[string]entryReader{}
var err error
if c.IsVC() {
logs[vpxdKey] = dlogReader{c, vpxdKey, nil}
var hosts []*object.HostSystem
if c.Cluster == nil && c.Host != nil {
hosts = []*object.HostSystem{c.Host}
} else {
hosts, err = c.Datastore.AttachedClusterHosts(context.TODO(), c.Cluster)
if err != nil {
return nil, err
}
}
for _, host := range hosts {
lname := fmt.Sprintf("%s/%s", hostdKey, host)
logs[lname] = dlogReader{c, hostdKey, host}
}
} else {
logs[hostdKey] = dlogReader{c, hostdKey, nil}
}
return logs, nil
}
func tarEntries(readers map[string]entryReader, out io.Writer) error {
defer trace.End(trace.Begin(""))
r, w := io.Pipe()
t := tar.NewWriter(w)
wg := new(sync.WaitGroup)
wg.Add(1)
// stream tar to out
go func() {
_, err := io.Copy(out, r)
if err != nil {
log.Errorf("error copying tar: %s", err)
}
wg.Done()
}()
for name, r := range readers {
log.Infof("Collecting log with reader %s(%#v)", name, r)
e, err := r.open()
if err != nil {
log.Warningf("error reading %s(%s): %s\n", name, r, err)
continue
}
var sz int64
if e != nil {
sz = e.Size()
}
header := tar.Header{
Name: name,
Size: sz,
Mode: 0640,
ModTime: time.Now(),
}
err = t.WriteHeader(&header)
if err != nil {
log.Errorf("Failed to write header for %s: %s", header.Name, err)
continue
}
log.Infof("%s has size %d", header.Name, header.Size)
// be explicit about the number of bytes to copy as the log files will likely
// be written to during this exercise
if e != nil {
_, err = io.CopyN(t, e, sz)
// #nosec: Errors unhandled.
_ = e.Close()
}
if err != nil {
log.Errorf("Failed to write content for %s: %s", header.Name, err)
continue
}
}
// #nosec: Errors unhandled.
_ = t.Flush()
// #nosec: Errors unhandled.
_ = w.Close()
wg.Wait()
// #nosec: Errors unhandled.
_ = r.Close()
return nil
}
func zipEntries(readers map[string]entryReader, out *zip.Writer) error {
defer trace.End(trace.Begin(""))
defer out.Close()
defer out.Flush()
for name, r := range readers {
log.Infof("Collecting log with reader %s(%#v)", name, r)
e, err := r.open()
if err != nil {
log.Warningf("error reading %s(%s): %s\n", name, r, err)
}
var sz int64
if e != nil {
sz = e.Size()
}
header := &zip.FileHeader{
Name: name,
Method: zip.Deflate,
}
header.SetModTime(time.Now())
header.SetMode(0644)
if sz > uint32max {
header.UncompressedSize = uint32max
} else {
header.UncompressedSize = uint32(sz)
}
w, err := out.CreateHeader(header)
if err != nil {
log.Errorf("Failed to create Zip writer for %s: %s", header.Name, err)
continue
}
log.Infof("%s has size %d", header.Name, sz)
// be explicit about the number of bytes to copy as the log files will likely
// be written to during this exercise
if e != nil {
_, err = io.CopyN(w, e, sz)
// #nosec: Errors unhandled.
_ = e.Close()
}
if err != nil {
log.Errorf("Failed to write content for %s: %s", header.Name, err)
continue
}
log.Infof("Wrote %d bytes to %s", sz, header.Name)
}
return nil
}
func tailFile(wr io.Writer, file string, done *chan bool) error {
defer trace.End(trace.Begin(file))
// By default, seek to EOF (if file doesn't exist)
spos := tail.SeekInfo{
Offset: 0,
Whence: 2,
}
// If the file exists, we want to go back tailLines lines
// and pass that new offset into the TailFile() constructor
// Per @fdawg4l, use bytes.LastIndex() and a 1k buffer to reduce
// seeks/reads
f, err := os.Open(file)
if err == nil {
spos = tail.SeekInfo{
Offset: findSeekPos(f),
Whence: 0,
}
}
tcfg := tail.Config{
Location: &spos,
ReOpen: true,
MustExist: false,
Follow: true,
}
t, err := tail.TailFile(file, tcfg)
if err != nil {
return err
}
// We KNOW there's a data race here.
// But it doesn't break anything, so we just trap it.
defer func() {
t.Stop()
_ = recover()
}()
for true {
select {
case l := <-t.Lines:
if l.Err != nil {
return l.Err
}
fmt.Fprint(wr, l.Text, "\n")
case _ = <-*done:
return nil
}
}
return nil
}
// Find the offset we want to start tailing from.
// This should either be beginning-of-file or tailLines
// newlines from the EOF.
func findSeekPos(f *os.File) int64 {
defer trace.End(trace.Begin(""))
nlines := tailLines
readPos, err := f.Seek(0, 2)
// If for some reason we can't seek, we will just start tailing from beginning-of-file
if err != nil {
return int64(0)
}
// Buffer so we can seek nBytes (default: 1k) at a time
buf := make([]byte, nBytes)
for readPos > 0 {
// Go back nBytes from the last readPos we've seen (stopping at beginning-of-file)
// and read the next nBytes
readPos -= int64(len(buf))
if readPos < 0 {
// We don't want to overlap our read with previous reads...
buf = buf[:(int(readPos) + nBytes)]
readPos = 0
}
bufend, err := f.ReadAt(buf, readPos)
// It's OK to get io.EOF here. Anything else is bad.
if err != nil && err != io.EOF {
log.Errorf("Error reading from file %s: %s", f.Name(), err)
return 0
}
// Start from the end of the buffer and start looking for newlines
for bufend > 0 {
bufend = bytes.LastIndexByte(buf[:bufend], '\n')
if bufend < 0 {
break
}
nlines--
if nlines < 0 {
return readPos + int64(bufend) + 1
}
}
}
return 0
}

575
vendor/github.com/vmware/vic/cmd/vicadmin/server.go generated vendored Normal file
View File

@@ -0,0 +1,575 @@
// Copyright 2016-2017 VMware, 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 main
import (
"archive/zip"
"compress/gzip"
"crypto/tls"
"crypto/x509"
"html/template"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/docker/go-connections/tlsconfig"
"github.com/google/uuid"
gorillacontext "github.com/gorilla/context"
"github.com/vmware/vic/lib/vicadmin"
"github.com/vmware/vic/pkg/filelock"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
)
type server struct {
l net.Listener
addr string
mux *http.ServeMux
uss *UserSessionStore
}
// LoginPageData contains items needed to render the login page template
type LoginPageData struct {
Hostname string
SystemTime string
InvalidLogin string
}
type format int
const (
formatTGZ format = iota
formatZip
)
var beginningOfTime = time.Unix(0, 0).Format(time.RFC3339)
const (
sessionExpiration = time.Hour * 24
sessionCookieKey = "session-data"
sessionCreationTimeKey = "created"
sessionKey = "session-id"
ipAddressKey = "ip"
loginPagePath = "/authentication"
authFailure = loginPagePath + "?unauthorized"
genericErrorMessage = "Internal Server Error; see /var/log/vic/vicadmin.log for details" // for http errors that shouldn't be displayed in the browser to the user
)
func (s *server) listen() error {
defer trace.End(trace.Begin(""))
var err error
var certificate *tls.Certificate
s.uss = NewUserSessionStore()
if vchConfig.HostCertificate != nil {
certificate, err = vchConfig.HostCertificate.Certificate()
} else {
var c tls.Certificate
if c, err = tls.X509KeyPair(
rootConfig.serverCert.Cert.Bytes(),
rootConfig.serverCert.Key.Bytes()); err != nil {
log.Errorf("Could not generate self-signed certificate for vicadmin running with due to error %s", err.Error())
return err
}
certificate = &c
}
if err != nil {
log.Errorf("Could not load certificate from config - running without TLS: %s", err)
s.l, err = net.Listen("tcp", s.addr)
return err
}
// FIXME: assignment copies lock value to tlsConfig: crypto/tls.Config contains sync.Once contains sync.Mutex
tlsconfig := func(c *tls.Config) *tls.Config {
// if there are CAs, then TLS is enabled
if len(vchConfig.CertificateAuthorities) != 0 {
if c.ClientCAs == nil {
c.ClientCAs = x509.NewCertPool()
}
if !c.ClientCAs.AppendCertsFromPEM(vchConfig.CertificateAuthorities) {
log.Errorf("Unable to load CAs from config; client auth via certificate will not function")
}
c.ClientAuth = tls.VerifyClientCertIfGiven
} else {
log.Warnf("No certificate authorities found for certificate-based authentication. This may be intentional, however, certificate-based authentication is disabled")
}
// #nosec: TLS InsecureSkipVerify may be true
return &tls.Config{
Certificates: c.Certificates,
NameToCertificate: c.NameToCertificate,
GetCertificate: c.GetCertificate,
RootCAs: c.RootCAs,
NextProtos: c.NextProtos,
ServerName: c.ServerName,
ClientAuth: c.ClientAuth,
ClientCAs: c.ClientCAs,
InsecureSkipVerify: c.InsecureSkipVerify,
CipherSuites: c.CipherSuites,
PreferServerCipherSuites: c.PreferServerCipherSuites,
SessionTicketsDisabled: c.SessionTicketsDisabled,
SessionTicketKey: c.SessionTicketKey,
ClientSessionCache: c.ClientSessionCache,
MinVersion: tls.VersionTLS12,
MaxVersion: c.MaxVersion,
CurvePreferences: c.CurvePreferences,
}
}(tlsconfig.ServerDefault())
tlsconfig.Certificates = []tls.Certificate{*certificate}
innerListener, err := net.Listen("tcp", s.addr)
if err != nil {
log.Fatal(err)
return err
}
s.l = tls.NewListener(innerListener, tlsconfig)
return nil
}
func (s *server) listenPort() int {
return s.l.Addr().(*net.TCPAddr).Port
}
// Enforces authentication on route `link` and runs `handler` on successful auth
func (s *server) AuthenticatedHandle(link string, h http.Handler) {
s.Authenticated(link, h.ServeHTTP)
}
func (s *server) Handle(link string, h http.Handler) {
log.Debugf("%s --- %s", time.Now().String(), link)
s.mux.Handle(link, gorillacontext.ClearHandler(h))
}
// Enforces authentication on route `link` and runs `handler` on successful auth
func (s *server) Authenticated(link string, handler func(http.ResponseWriter, *http.Request)) {
defer trace.End(trace.Begin(""))
authHandler := func(w http.ResponseWriter, r *http.Request) {
// #nosec: Errors unhandled because it is okay if the cookie doesn't exist.
websession, _ := s.uss.cookies.Get(r, sessionCookieKey)
if len(r.TLS.PeerCertificates) > 0 {
// the user is authenticated by certificate at connection time
log.Infof("Authenticated connection via client certificate with serial %s from %s", r.TLS.PeerCertificates[0].SerialNumber, r.RemoteAddr)
key := uuid.New().String()
vs, err := vSphereSessionGet(&rootConfig.Config)
if err != nil {
log.Errorf("Unable to get vSphere session with default config for cert-auth'd user")
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
usersess := s.uss.Add(key, &rootConfig.Config, vs)
timeNow, err := usersess.created.MarshalText()
if err != nil {
// it's probably safe to ignore this error since we just created usersess.created when we called Add() above
// but just in case..
log.Errorf("Failed to unmarshal time object %+v into text due to error: %s", usersess.created, err)
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
websession.Values[sessionCreationTimeKey] = string(timeNow)
websession.Values[sessionKey] = key
remoteAddr := strings.SplitN(r.RemoteAddr, ":", 2)
if len(remoteAddr) != 2 { // TODO: ctrl+f RemoteAddr and move this routine to helper
log.Errorf("Format of IP address %s (should be IP:PORT) not recognized", r.RemoteAddr)
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
websession.Values[ipAddressKey] = remoteAddr[0]
err = websession.Save(r, w)
if err != nil {
log.Errorf("Could not create session for user authenticated via client certificate due to error \"%s\"", err.Error())
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
// user was authenticated via cert
handler(w, r)
return
}
c := websession.Values[sessionCreationTimeKey]
if c == nil { // no cookie, so redirect to login
log.Infof("No authentication token: %+v", websession.Values)
http.Redirect(w, r, loginPagePath, http.StatusSeeOther)
return
}
// here we have a cookie, but we need to make sure it's not expired:
// parse the cookie creation time
created, err := time.Parse(time.RFC3339, c.(string))
if err != nil {
// we pulled this value out of a cookie, so if it doesn't parse, it might've been tampered with
// though the cookie's encrypted so that would destroy the whole cookie..
// Handling the error in any case:
log.Errorf("Couldn't parse time out of retrieved cookie due to error %s", err.Error())
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
// cookie exists but is expired
if time.Since(created) > sessionExpiration {
http.Redirect(w, r, loginPagePath, http.StatusTemporaryRedirect)
return
}
// verify that the auth token is being used by the same IP it was created for
c = websession.Values[ipAddressKey]
if c == nil {
log.Errorf("Couldn't get IP address out of cookie for user connecting from %s at %s", r.RemoteAddr, time.Now())
http.Redirect(w, r, loginPagePath, http.StatusTemporaryRedirect)
return
}
connectingAddr := strings.SplitN(r.RemoteAddr, ":", 2)
if len(connectingAddr) != 2 { // TODO: ctrl+f r.RemoteAddr and move this routine to helper
log.Errorf("Format of IP address %s (should be IP:PORT) not recognized", r.RemoteAddr)
http.Error(w, genericErrorMessage, http.StatusInternalServerError)
return
}
if c.(string) != connectingAddr[0] {
log.Warnf("User with a valid auth cookie from %s has reappeared at %s. Their token will be expired.", c.(string), connectingAddr[0])
s.logoutHandler(w, r)
return
}
// if the date & remote IP on the cookie were valid, then the user is authenticated
log.Infof("User with a valid auth cookie at %s is authenticated.", connectingAddr[0])
handler(w, r)
}
s.Handle(link, http.HandlerFunc(authHandler))
}
// renders the page for login and handles authorization requests
func (s *server) loginPage(res http.ResponseWriter, req *http.Request) {
defer trace.End(trace.Begin(""))
var invalidLoginMessage = ""
if req.Method == "POST" {
// take the form data and use it to try to authenticate with vsphere
// create a userconfig
userconfig := session.Config{
Insecure: false,
Thumbprint: rootConfig.Thumbprint,
Keepalive: rootConfig.Keepalive,
ClusterPath: rootConfig.ClusterPath,
DatacenterPath: rootConfig.DatacenterPath,
DatastorePath: rootConfig.DatastorePath,
HostPath: rootConfig.Config.HostPath,
PoolPath: rootConfig.PoolPath,
User: url.UserPassword(req.FormValue("username"), req.FormValue("password")),
Service: rootConfig.Service,
}
// check login
vs, err := vSphereSessionGet(&userconfig)
if err != nil || vs == nil {
// something went wrong or we could not authenticate
log.Warnf("%s failed to authenticate ", req.RemoteAddr)
invalidLoginMessage = "Authentication failed due to incorrect credential(s)"
hostName, err := os.Hostname()
if err != nil {
hostName = "VCH"
}
loginPageData := &LoginPageData{
Hostname: hostName,
SystemTime: time.Now().Format(time.UnixDate),
InvalidLogin: invalidLoginMessage,
}
tmpl, err := template.ParseFiles("auth.html")
err = tmpl.ExecuteTemplate(res, "auth.html", loginPageData)
if err != nil {
log.Errorf("Error parsing template: %s", err)
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
return
}
// successful login above; user is authenticated, reported for audit purposes
log.Debugf("User %s from %s was successfully authenticated", userconfig.User.Username(), req.RemoteAddr)
// create a token to save as an encrypted & signed cookie
websession, err := s.uss.cookies.Get(req, sessionCookieKey)
if websession == nil {
log.Errorf("Web session object could not be created due to error %s", err)
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
key := uuid.New().String()
userconfig.User = nil
userconfig.Service = ""
us := s.uss.Add(key, &userconfig, vs)
timeNow, err := us.created.MarshalText()
if err != nil {
log.Errorf("Failed to unmarshal time object %+v into text due to error: %s", us.created, err)
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
websession.Values[sessionCreationTimeKey] = string(timeNow)
websession.Values[sessionKey] = key
remoteAddr := strings.SplitN(req.RemoteAddr, ":", 2)
if len(remoteAddr) != 2 { // TODO: ctrl+f RemoteAddr and move this routine to helper
log.Errorf("Format of IP address %s (should be IP:PORT) not recognized", req.RemoteAddr)
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
websession.Values[ipAddressKey] = remoteAddr[0]
if err := websession.Save(req, res); err != nil {
log.Errorf("\"%s\" occurred while trying to save session to browser", err.Error())
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
// redirect to dashboard
http.Redirect(res, req, "/", http.StatusSeeOther)
return
}
// Render login page (shows up on non-POST requests)
hostName, err := os.Hostname()
if err != nil {
hostName = "VCH"
}
loginPageData := &LoginPageData{
Hostname: hostName,
SystemTime: time.Now().Format(time.UnixDate),
InvalidLogin: invalidLoginMessage,
}
tmpl, err := template.ParseFiles("auth.html")
err = tmpl.ExecuteTemplate(res, "auth.html", loginPageData)
if err != nil {
log.Errorf("Error parsing template: %s", err)
http.Error(res, genericErrorMessage, http.StatusInternalServerError)
return
}
}
func (s *server) serve() error {
defer trace.End(trace.Begin(""))
s.mux = http.NewServeMux()
// unauthenticated routes
// these assets bypass authentication & are world-readable
s.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("css/"))))
s.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.Dir("images/"))))
s.Handle("/fonts/", http.StripPrefix("/fonts/", http.FileServer(http.Dir("fonts/"))))
s.Handle(loginPagePath, http.HandlerFunc(s.loginPage))
// authenticated routes
// tar of appliance system logs
s.Authenticated("/logs.tar.gz", s.tarDefaultLogs)
s.Authenticated("/logs.zip", s.zipDefaultLogs)
// tar of appliance system logs + container logs
s.Authenticated("/container-logs.tar.gz", s.tarContainerLogs)
s.Authenticated("/container-logs.zip", s.zipContainerLogs)
// get single log file (no tail)
s.Authenticated("/logs/", func(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.URL.Path, "/logs/")
log.Debugf("writing contents for %s", file)
writeLogFiles(w, r, file, true)
})
// get single log file (with tail)
s.Authenticated("/logs/tail/", func(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.URL.Path, "/logs/tail/")
log.Debugf("writing contents for %s", file)
writeLogFiles(w, r, file, false)
s.tailFiles(w, r, []string{filepath.Join(logFileDir, file)})
})
s.Authenticated("/logout", s.logoutHandler)
s.Authenticated("/", s.index)
server := &http.Server{
Handler: s.mux,
}
return server.Serve(s.l)
}
func (s *server) stop() error {
defer trace.End(trace.Begin(""))
if s.l != nil {
err := s.l.Close()
s.l = nil
return err
}
return nil
}
// logout handler expires the user's session cookie by setting its creation time to the beginning of time
func (s *server) logoutHandler(res http.ResponseWriter, req *http.Request) {
// #nosec: Errors unhandled.
websession, _ := s.uss.cookies.Get(req, sessionCookieKey)
// ignore parsing/marshalling errors because we're parsing a hardcoded beginning-of-time string
websession.Values[sessionCreationTimeKey] = beginningOfTime
if err := websession.Save(req, res); err != nil {
http.Error(res, "Failed to expire user session", http.StatusInternalServerError)
return
}
s.uss.Delete(websession.Values[sessionKey].(string))
http.Redirect(res, req, "/authentication", http.StatusTemporaryRedirect)
}
func (s *server) bundleContainerLogs(res http.ResponseWriter, req *http.Request, f format) {
defer trace.End(trace.Begin(""))
logrotateLock := filelock.NewFileLock(filelock.LogRotateLockName)
if err := logrotateLock.Acquire(); err != nil {
log.Errorf("Failed to acquire logrotate lock: %s", err)
} else {
defer func() { logrotateLock.Release() }()
}
readers := configureReaders()
c, err := s.getSessionFromRequest(context.Background(), req)
if err != nil {
log.Errorf("Failed to get vSphere session while bundling container logs due to error: %s", err.Error())
http.Redirect(res, req, "/logout", http.StatusTemporaryRedirect)
return
}
logs, err := findDatastoreLogs(c)
if err != nil {
log.Warningf("error searching datastore: %s", err)
} else {
for key, rdr := range logs {
readers[key] = rdr
}
}
logs, err = findDiagnosticLogs(c)
if err != nil {
log.Warningf("error collecting diagnostic logs: %s", err)
} else {
for key, rdr := range logs {
readers[key] = rdr
}
}
s.bundleLogs(res, req, readers, f)
}
func (s *server) tarDefaultLogs(res http.ResponseWriter, req *http.Request) {
defer trace.End(trace.Begin(""))
s.bundleLogs(res, req, configureReaders(), formatTGZ)
}
func (s *server) zipDefaultLogs(res http.ResponseWriter, req *http.Request) {
defer trace.End(trace.Begin(""))
s.bundleLogs(res, req, configureReaders(), formatZip)
}
func (s *server) bundleLogs(res http.ResponseWriter, req *http.Request, readers map[string]entryReader, f format) {
defer trace.End(trace.Begin(""))
var err error
if f == formatTGZ {
res.Header().Set("Content-Type", "application/x-gzip")
z := gzip.NewWriter(res)
defer z.Close()
err = tarEntries(readers, z)
} else if f == formatZip {
res.Header().Set("Content-Type", "application/zip")
z := zip.NewWriter(res)
defer z.Close()
err = zipEntries(readers, z)
}
if err != nil {
log.Errorf("Error bundling logs: %s", err)
}
}
func (s *server) tarContainerLogs(res http.ResponseWriter, req *http.Request) {
s.bundleContainerLogs(res, req, formatTGZ)
}
func (s *server) zipContainerLogs(res http.ResponseWriter, req *http.Request) {
s.bundleContainerLogs(res, req, formatZip)
}
func (s *server) tailFiles(res http.ResponseWriter, req *http.Request, names []string) {
defer trace.End(trace.Begin(""))
cc := res.(http.CloseNotifier).CloseNotify()
fw := &flushWriter{
f: res.(http.Flusher),
w: res,
}
done := make(chan bool)
for _, file := range names {
go tailFile(fw, file, &done)
}
<-cc
for range names {
done <- true
}
}
func (s *server) index(res http.ResponseWriter, req *http.Request) {
defer trace.End(trace.Begin(""))
ctx := context.Background()
sess, err := s.getSessionFromRequest(ctx, req)
if err != nil {
log.Errorf("While loading index page got %s looking up a vSphere session", err.Error())
http.Redirect(res, req, "/logout", http.StatusTemporaryRedirect)
return
}
v := vicadmin.NewValidator(ctx, &vchConfig, sess)
if sess == nil {
// We're unable to connect to vSphere, so display an error message
v.VCHIssues = template.HTML("<span class=\"error-message\">We're having some trouble communicating with vSphere. <a href=\"/logout\">Logging in again</a> may resolve the issue.</span>\n")
}
tmpl, err := template.ParseFiles("dashboard.html")
err = tmpl.ExecuteTemplate(res, "dashboard.html", v)
if err != nil {
log.Errorf("Error parsing template: %s", err)
}
}

View File

@@ -0,0 +1,133 @@
// Copyright 2016 VMware, 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 main
import (
"fmt"
"sync"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/vmware/vic/pkg/vsphere/session"
)
// UserSession holds a user's session metadata
type UserSession struct {
id string
created time.Time
config *session.Config
vsphere *session.Session
}
// UserSessionStore holds and manages user sessions
type UserSessionStore struct {
mutex sync.RWMutex
sessions map[string]*UserSession
ticker *time.Ticker
cookies *sessions.CookieStore
}
type UserSessionStorer interface {
Add(id string, config *session.Config, vs *session.Session) *UserSession
Delete(id string)
VSphere(id string) (vSphereSession *session.Session, err error)
UserSession(id string) *UserSession
}
// Add a session. VS may be nil if host is plain ESX
func (u *UserSessionStore) Add(id string, config *session.Config, vs *session.Session) *UserSession {
u.mutex.Lock()
defer u.mutex.Unlock()
sess := &UserSession{
id: id,
created: time.Now(),
// TODO strip out config cause it's not needed anymore, but shows up in a number of places
config: config,
vsphere: vs,
}
u.sessions[id] = sess
return sess
}
func (u *UserSessionStore) Delete(id string) {
u.mutex.Lock()
defer u.mutex.Unlock()
delete(u.sessions, id)
}
// Grabs the UserSession metadata object and doesn't establish a connection to vSphere
func (u *UserSessionStore) UserSession(id string) *UserSession {
u.mutex.RLock()
defer u.mutex.RUnlock()
return u.sessions[id]
}
// Returns a vSphere session object. Caller responsible for error handling/logout
func (u *UserSessionStore) VSphere(ctx context.Context, id string) (*session.Session, error) {
us := u.UserSession(id)
if us == nil {
return nil, fmt.Errorf("User session with unique ID %s does not exist", id)
}
if us.vsphere == nil {
return nil, fmt.Errorf("No vSphere session found for user: %s", id)
}
vsphus, err := us.vsphere.SessionManager.UserSession(ctx)
if err != nil || vsphus == nil {
if err != nil {
log.Warnf("Failed to validate user %s session: %v", id, err)
return nil, err
}
return nil, fmt.Errorf("User %s session has expired", id)
}
log.Infof("Found vSphere session for vicadmin usersession %s", id)
return us.vsphere, nil
}
// reaper takes abandoned sessions to a farm upstate so they don't build up forever
func (u *UserSessionStore) reaper() {
for range u.ticker.C {
for id, session := range u.sessions {
if time.Since(session.created) > sessionExpiration {
u.Delete(id)
}
}
}
}
// NewUserSessionStore creates & initializes a UserSessionStore and starts a session reaper in the background
func NewUserSessionStore() *UserSessionStore {
u := &UserSessionStore{
sessions: make(map[string]*UserSession),
ticker: time.NewTicker(time.Minute * 10),
mutex: sync.RWMutex{},
cookies: sessions.NewCookieStore(
[]byte(securecookie.GenerateRandomKey(64)),
[]byte(securecookie.GenerateRandomKey(32))),
}
u.cookies.Options = &sessions.Options{
Path: "/",
MaxAge: int(sessionExpiration.Seconds()),
Secure: true,
HttpOnly: true,
}
go u.reaper()
return u
}

634
vendor/github.com/vmware/vic/cmd/vicadmin/vicadm.go generated vendored Normal file
View File

@@ -0,0 +1,634 @@
// Copyright 2016-2017 VMware, 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 main
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
vchconfig "github.com/vmware/vic/lib/config"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/pprof"
"github.com/vmware/vic/pkg/certificate"
viclog "github.com/vmware/vic/pkg/log"
"github.com/vmware/vic/pkg/log/syslog"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/version"
"github.com/vmware/vic/pkg/vsphere/compute"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
timeout = time.Duration(2 * time.Second)
)
type serverCertificate struct {
Key bytes.Buffer
Cert bytes.Buffer
}
type vicAdminConfig struct {
session.Config
addr string
tls bool
serverCert *serverCertificate
}
var (
logFileDir = "/var/log/vic"
logFileListPrefixes = []string{
"docker-personality.log",
"port-layer.log",
"vicadmin.log",
"init.log",
"kubelet-starter.log",
"virtual-kubelet.log",
}
// VMFiles is the set of files to collect per VM associated with the VCH
vmFiles = []string{
"output.log",
"vmware.log",
"tether.debug",
}
// this struct holds root credentials or vSphere extension private key instead if available
// if you are exposing log information to a user, create a new session for that user, do not use this one
// also, 'root' is a pun -- this is both the "root" config, e.g., the base config, and the one w/ root creds
rootConfig vicAdminConfig
resources vchconfig.Resources
vchConfig vchconfig.VirtualContainerHostConfigSpec
datastore types.ManagedObjectReference
)
type logfile struct {
URL url.URL
VMName string
Host *object.HostSystem
}
func Init() {
// #nosec: Errors unhandled.
_ = pprof.StartPprof("vicadmin", pprof.VicadminPort)
defer trace.End(trace.Begin(""))
// load the vch config
src, err := extraconfig.GuestInfoSource()
if err != nil {
log.Errorf("Unable to load configuration from guestinfo")
return
}
extraconfig.Decode(src, &vchConfig)
logcfg := viclog.NewLoggingConfig()
if vchConfig.Diagnostics.DebugLevel > 0 {
logcfg.Level = log.DebugLevel
trace.Logger.Level = log.DebugLevel
syslog.Logger.Level = log.DebugLevel
}
if vchConfig.Diagnostics.SysLogConfig != nil {
logcfg.Syslog = &viclog.SyslogConfig{
Network: vchConfig.Diagnostics.SysLogConfig.Network,
RAddr: vchConfig.Diagnostics.SysLogConfig.RAddr,
Priority: syslog.Info | syslog.Daemon,
}
}
viclog.Init(logcfg)
trace.InitLogger(logcfg)
// We don't want to run this as root.
ud := syscall.Getuid()
gd := syscall.Getgid()
log.Info(fmt.Sprintf("Current UID/GID = %d/%d", ud, gd))
// TODO: Enable this after we figure out to NOT break the test suite with it.
// if ud == 0 {
// log.Errorf("Error: vicadmin must not run as root.")
// time.Sleep(60 * time.Second)
// os.Exit(1)
// }
flag.StringVar(&rootConfig.addr, "l", "client.localhost:2378", "Listen address")
// TODO: This should all be pulled from the config
flag.StringVar(&rootConfig.DatacenterPath, "dc", "", "Path of the datacenter")
flag.StringVar(&rootConfig.ClusterPath, "cluster", "", "Path of the cluster")
flag.StringVar(&rootConfig.PoolPath, "pool", "", "Path of the resource pool")
if vchConfig.HostCertificate == nil {
log.Infoln("--no-tls is enabled on the personality")
rootConfig.serverCert = &serverCertificate{}
rootConfig.serverCert.Cert, rootConfig.serverCert.Key, err = certificate.CreateSelfSigned(rootConfig.addr, []string{"VMware, Inc."}, 2048)
if err != nil {
log.Errorf("--no-tls was specified but we couldn't generate a self-signed cert for vic admin due to error %s so vicadmin will not run", err.Error())
return
}
}
// FIXME: pull the rest from flags
flag.Parse()
}
type entryReader interface {
open() (entry, error)
}
type entry interface {
io.ReadCloser
Name() string
Size() int64
}
type bytesEntry struct {
io.ReadCloser
name string
size int64
}
func (e *bytesEntry) Name() string {
return e.name
}
func (e *bytesEntry) Size() int64 {
return e.size
}
func newBytesEntry(name string, b []byte) entry {
r := bytes.NewReader(b)
return &bytesEntry{
ReadCloser: ioutil.NopCloser(r),
size: int64(r.Len()),
name: name,
}
}
type versionReader string
func (path versionReader) open() (entry, error) {
defer trace.End(trace.Begin(string(path)))
return newBytesEntry(string(path), []byte(version.GetBuild().ShortVersion())), nil
}
type commandReader string
func (path commandReader) open() (entry, error) {
defer trace.End(trace.Begin(string(path)))
args := strings.Split(string(path), " ")
// #nosec: Subprocess launching with variable
cmd := exec.Command(args[0], args[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%s: %s", err, string(output))
}
return newBytesEntry(string(path), output), nil
}
type fileReader string
type fileEntry struct {
io.ReadCloser
os.FileInfo
}
func (path fileReader) open() (entry, error) {
defer trace.End(trace.Begin(string(path)))
f, err := os.Open(string(path))
if err != nil {
return nil, err
}
s, err := os.Stat(string(path))
if err != nil {
return nil, err
}
// Files in /proc always have struct stat.st_size==0, so just read it into memory.
if s.Size() == 0 && strings.HasPrefix(f.Name(), "/proc/") {
b, err := ioutil.ReadAll(f)
// #nosec: Errors unhandled.
_ = f.Close()
if err != nil {
return nil, err
}
return newBytesEntry(f.Name(), b), nil
}
return &fileEntry{
ReadCloser: f,
FileInfo: s,
}, nil
}
type urlReader string
func httpEntry(name string, res *http.Response) (entry, error) {
defer trace.End(trace.Begin(name))
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
if res.ContentLength > 0 {
return &bytesEntry{
ReadCloser: res.Body,
size: res.ContentLength,
name: name,
}, nil
}
// If we don't have Content-Length, read into memory for the tar.Header.Size
body, err := ioutil.ReadAll(res.Body)
// #nosec: Errors unhandled.
_ = res.Body.Close()
if err != nil {
return nil, err
}
return newBytesEntry(name, body), nil
}
func (path urlReader) open() (entry, error) {
defer trace.End(trace.Begin(string(path)))
client := http.Client{
Timeout: timeout,
}
res, err := client.Get(string(path))
if err != nil {
return nil, err
}
return httpEntry(string(path), res)
}
type datastoreReader struct {
ds *object.Datastore
path string
ctx context.Context
}
// listVMPaths returns an array of datastore paths for VMs associated with the
// VCH - this includes containerVMs and the appliance
func listVMPaths(ctx context.Context, s *session.Session) ([]logfile, error) {
defer trace.End(trace.Begin(""))
var err error
var children []*vm.VirtualMachine
if len(vchConfig.ComputeResources) == 0 {
return nil, errors.New("compute resources is empty")
}
ref := vchConfig.ComputeResources[0]
rp := compute.NewResourcePool(ctx, s, ref)
if children, err = rp.GetChildrenVMs(ctx, s); err != nil {
return nil, err
}
self, err := guest.GetSelf(ctx, s)
if err != nil {
log.Errorf("Unable to get handle to self for log filtering")
}
log.Infof("Found %d candidate VMs in resource pool %s for log collection", len(children), ref.String())
logfiles := []logfile{}
for _, child := range children {
path, err := child.VMPathNameAsURL(ctx)
if err != nil {
log.Errorf("Unable to get datastore path for child VM %s: %s", child.Reference(), err)
// we need to get as many logs as possible
continue
}
logname, err := child.Name(ctx)
if err != nil {
log.Errorf("Unable to get the vm name for %s: %s", child.Reference(), err)
continue
}
if self != nil && child.Reference().String() == self.Reference().String() {
// FIXME: until #2630 is addressed, and we confirm this filters secrets from appliance vmware.log as well,
// we're skipping direct collection of those logs.
log.Info("Skipping collection for appliance VM (moref match)")
continue
}
// backup check if we were unable to initialize self for some reason
if self == nil && logname == vchConfig.Name {
log.Info("Skipping collection for appliance VM (string match)")
continue
}
log.Debugf("Adding VM for log collection: %s", path.String())
h, err := child.HostSystem(ctx)
if err != nil {
log.Warnf("Unable to get host system for VM %s - will use default host for log collection: %s", logname, err)
}
log := logfile{
URL: path,
VMName: logname,
Host: h,
}
logfiles = append(logfiles, log)
}
log.Infof("Collecting logs from %d VMs", len(logfiles))
log.Infof("Found VM paths are : %#v", logfiles)
return logfiles, nil
}
// addApplianceLogs whitelists the logs to include for the appliance.
// TODO: once we've started encrypting all potentially sensitive data and filtering out guestinfo.ovfEnv
// we can resume collection of vmware.log and drop the appliance specific handling
func addApplianceLogs(ctx context.Context, s *session.Session, readers map[string]entryReader) error {
self, err := guest.GetSelf(ctx, s)
if err != nil || self == nil {
return fmt.Errorf("Unable to collect appliance logs due to unknown self-reference: %s", err)
}
self2 := vm.NewVirtualMachineFromVM(ctx, s, self)
path, err := self2.VMPathNameAsURL(ctx)
if err != nil {
return err
}
ds, err := s.Finder.Datastore(ctx, path.Host)
if err != nil {
return err
}
h, err := self2.HostSystem(ctx)
if err != nil {
log.Warnf("Unable to get host system for appliance - will use default host for log collection: %s", err)
} else {
ctx = ds.HostContext(ctx, h)
}
wpath := fmt.Sprintf("appliance/tether.debug")
rpath := fmt.Sprintf("%s/%s", path.Path, "tether.debug")
log.Infof("Processed File read Path : %s", rpath)
log.Infof("Processed File write Path : %s", wpath)
readers[wpath] = datastoreReader{
ds: ds,
path: rpath,
ctx: ctx,
}
return nil
}
// find datastore logs for the appliance itself and all containers
func findDatastoreLogs(c *session.Session) (map[string]entryReader, error) {
defer trace.End(trace.Begin(""))
// Create an empty reader as opposed to a nil reader...
readers := map[string]entryReader{}
ctx := context.Background()
logfiles, err := listVMPaths(ctx, c)
if err != nil {
detail := fmt.Sprintf("unable to perform datastore log collection due to failure looking up paths: %s", err)
log.Error(detail)
return nil, errors.New(detail)
}
err = addApplianceLogs(ctx, c, readers)
if err != nil {
log.Errorf("Issue collecting appliance logs: %s", err)
}
for _, logfile := range logfiles {
log.Debugf("Assembling datastore readers for %s", logfile.URL.String())
// obtain datastore object
ds, err := c.Finder.Datastore(ctx, logfile.URL.Host)
if err != nil {
log.Errorf("Failed to acquire reference to datastore %s: %s", logfile.URL.Host, err)
continue
}
hCtx := ctx
if logfile.Host != nil {
hCtx = ds.HostContext(ctx, logfile.Host)
}
// generate the full paths to collect
for _, file := range vmFiles {
wpath := fmt.Sprintf("%s/%s", logfile.VMName, file)
rpath := fmt.Sprintf("%s/%s", logfile.URL.Path, file)
log.Infof("Processed File read Path : %s", rpath)
log.Infof("Processed File write Path : %s", wpath)
readers[wpath] = datastoreReader{
ds: ds,
path: rpath,
ctx: hCtx,
}
log.Debugf("Added log file for collection: %s", logfile.URL.String())
}
}
return readers, nil
}
func (r datastoreReader) open() (entry, error) {
defer trace.End(trace.Begin(r.path))
u, ticket, err := r.ds.ServiceTicket(r.ctx, r.path, "GET")
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if ticket != nil {
req.AddCookie(ticket)
}
res, err := r.ds.Client().Do(req)
if err != nil {
return nil, err
}
return httpEntry(r.path, res)
}
// stripCredentials removes user credentials from "in"
func stripCredentials(in *session.Session) error {
serviceURL, err := soap.ParseURL(rootConfig.Service)
if err != nil {
log.Errorf("Error parsing service URL from config: %s", err)
return err
}
serviceURL.User = nil
newclient, err := govmomi.NewClient(context.Background(), serviceURL, true)
if err != nil {
log.Errorf("Error creating new govmomi client without credentials but with auth cookie: %s", err.Error())
return err
}
newclient.Jar = in.Client.Jar
in.Client = newclient
return nil
}
func vSphereSessionGet(sessconfig *session.Config) (*session.Session, error) {
s := session.NewSession(sessconfig)
s.UserAgent = version.UserAgent("vic-admin")
ctx := context.Background()
_, err := s.Connect(ctx)
if err != nil {
log.Warnf("Unable to connect: %s", err)
return nil, err
}
_, err = s.Populate(ctx)
if err != nil {
// not a critical error for vicadmin
log.Warnf("Unable to populate session: %s", err)
}
usersession, err := s.SessionManager.UserSession(ctx)
if err != nil {
log.Errorf("Got %s while creating user session", err)
return nil, err
}
if usersession == nil {
return nil, fmt.Errorf("vSphere session is no longer valid")
}
log.Infof("Got session from vSphere with key: %s username: %s", usersession.Key, usersession.UserName)
err = stripCredentials(s)
if err != nil {
return nil, err
}
return s, nil
}
func (s *server) getSessionFromRequest(ctx context.Context, r *http.Request) (*session.Session, error) {
sessionData, err := s.uss.cookies.Get(r, sessionCookieKey)
if err != nil {
return nil, err
}
var d interface{}
var ok bool
if d, ok = sessionData.Values[sessionKey]; !ok {
return nil, fmt.Errorf("User-provided cookie did not contain a session ID -- it is corrupt or tampered")
}
c, err := s.uss.VSphere(ctx, d.(string))
return c, err
}
type flushWriter struct {
f http.Flusher
w io.Writer
}
func (fw *flushWriter) Write(p []byte) (int, error) {
n, err := fw.w.Write(p)
fw.f.Flush()
return n, err
}
func main() {
Init()
if version.Show() {
fmt.Fprintf(os.Stdout, "%s\n", version.String())
return
}
// FIXME: these should just be consumed directly inside Session
rootConfig.Service = vchConfig.Target
rootConfig.User = url.UserPassword(vchConfig.Username, vchConfig.Token)
rootConfig.Thumbprint = vchConfig.TargetThumbprint
rootConfig.DatastorePath = vchConfig.Storage.ImageStores[0].Host
if vchConfig.Diagnostics.DebugLevel > 0 {
log.SetLevel(log.DebugLevel)
log.Info("Setting debug logging")
}
if vchConfig.Diagnostics.DebugLevel > 2 {
rootConfig.addr = "0.0.0.0:2378"
log.Warn("Listening on all networks because of debug level")
}
s := &server{
addr: rootConfig.addr,
}
err := s.listen()
if err != nil {
log.Fatal(err)
}
log.Infof("listening on %s", s.addr)
signals := []syscall.Signal{
syscall.SIGTERM,
syscall.SIGINT,
}
sigchan := make(chan os.Signal, 1)
for _, signum := range signals {
signal.Notify(sigchan, signum)
}
go func() {
signal := <-sigchan
log.Infof("received %s", signal)
s.stop()
}()
s.serve()
}

View File

@@ -0,0 +1,327 @@
// Copyright 2016-2018 VMware, 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 main
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
vchconfig "github.com/vmware/vic/lib/config"
"github.com/vmware/vic/pkg/version"
"github.com/vmware/vic/pkg/vsphere/test/env"
)
// use an http client which we modify in init()
// to be permissive with certificates so we can
// use a self-signed cert hardcoded into these tests
var insecureClient *http.Client
func init() {
// init needs to be updated to include client certificates
// so that the disabled tests can be re-enabled
sdk := env.URL(nil)
if sdk != "" {
flag.Set("sdk", sdk)
flag.Set("vm-path", "docker-appliance")
flag.Set("cluster", os.Getenv("GOVC_CLUSTER"))
}
// fake up a docker-host for pprof collection
u := url.URL{Scheme: "http", Host: "127.0.0.1:6060"}
go func() {
log.Println(http.ListenAndServe(u.Host, nil))
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
insecureClient = &http.Client{Transport: transport}
flag.Set("docker-host", u.Host)
hostCertFile := "fixtures/vicadmin_test_cert.pem"
hostKeyFile := "fixtures/vicadmin_test_pkey.pem"
cert, cerr := ioutil.ReadFile(hostCertFile)
key, kerr := ioutil.ReadFile(hostKeyFile)
if kerr != nil || cerr != nil {
panic("unable to load test certificate")
}
vchConfig.HostCertificate = &vchconfig.RawCertificate{
Cert: cert,
Key: key,
}
}
func TestLogFiles(t *testing.T) {
logFileNames := []string{}
for _, name := range logFiles() {
logFileNames = append(logFileNames, name)
}
fileCount := 0
//files should be in same order, otherwise we have evidence of a suspected race
for _, name := range logFiles() {
assert.Equal(t, name, logFileNames[fileCount])
fileCount++
}
}
func testLogTar(t *testing.T, plainHTTP bool) {
t.SkipNow() // TODO FIXME auth is in place now
if runtime.GOOS != "linux" {
t.SkipNow()
}
logFileDir = "."
s := &server{
addr: "127.0.0.1:0",
}
err := s.listen()
assert.NoError(t, err)
port := s.listenPort()
go s.serve()
defer s.stop()
var res *http.Response
res, err = insecureClient.Get(fmt.Sprintf("https://root:thisisinsecure@localhost:%d/container-logs.tar.gz", port))
if err != nil {
t.Fatal(err)
}
z, err := gzip.NewReader(res.Body)
if err != nil {
t.Fatal(err)
}
tz := tar.NewReader(z)
for {
h, err := tz.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
name, err := url.QueryUnescape(h.Name)
if err != nil {
t.Fatal(err)
}
if testing.Verbose() {
fmt.Printf("\n%s...\n", name)
io.CopyN(os.Stdout, tz, 150)
fmt.Printf("...\n")
}
}
}
func TestLogTar(t *testing.T) {
t.SkipNow() // TODO FIXME auth is in place now
testLogTar(t, false)
testLogTar(t, true)
}
func TestLogTail(t *testing.T) {
t.SkipNow() // TODO FIXME auth is in place now
if runtime.GOOS != "linux" {
t.SkipNow()
}
f, err := os.OpenFile("./vicadmin.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
f.WriteString("# not much here yet\n")
logFileDir = "."
name := filepath.Base(f.Name())
s := &server{
addr: "127.0.0.1:0",
// auth: &credentials{"root", "thisisinsecure"},
}
err = s.listen()
assert.NoError(t, err)
port := s.listenPort()
go s.serve()
defer s.stop()
out := ioutil.Discard
if testing.Verbose() {
out = os.Stdout
}
paths := []string{
"/logs/tail/" + name,
}
u := url.URL{
// User: url.UserPassword("root", "thisisinsecure"),
Scheme: "https",
Host: fmt.Sprintf("localhost:%d", port),
}
str := "The quick brown fox jumps over the lazy dog.\n"
// Pre-populate the log file
for i := 0; i < tailLines; i++ {
f.WriteString(str)
}
log.Printf("Testing TestLogTail\n")
for _, path := range paths {
u.Path = path
log.Printf("GET %s:\n", u.String())
res, err := insecureClient.Get(u.String())
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
// Each line written to the log file has enough bytes to ensure
// that 8 lines make up at least 256 bytes. This is in case this
// goroutine finishes writing all lines before tail kicks in.
go func() {
for j := 1; j < 512; j++ {
f.WriteString(str)
}
f.Sync()
}()
size := int64(256)
n, err := io.CopyN(out, res.Body, size)
assert.NoError(t, err)
out.Write([]byte("...\n"))
assert.Equal(t, size, n)
}
}
type seekTest struct {
input string
output int
}
func testSeek(t *testing.T, st seekTest, td string) {
f, err := ioutil.TempFile(td, "FindSeekPos")
defer f.Close()
if err != nil {
log.Printf("Unable to create temporary file: %s", err)
t.Fatal(err)
}
n, err := f.WriteString(st.input)
if err != nil {
t.Fatal(err)
}
if n != len(st.input) {
t.Fatal(fmt.Errorf("Incorrect byte count on write: %d/%d", n, len(st.input)))
}
if ret := findSeekPos(f); ret != int64(st.output) {
t.Fatal(fmt.Errorf("Incorrect seek position: %d/%d", ret, st.output))
}
log.Printf("Successfully seeked to position %d", st.output)
os.Remove(f.Name())
}
func TestFindSeekPos(t *testing.T) {
if runtime.GOOS != "linux" {
t.SkipNow()
}
seekTests := []seekTest{
{"abcd\nabcd\n", 0},
{"abcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\n", 10},
}
str := "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
str = fmt.Sprintf("%s%s", str, str)
str2 := fmt.Sprintf("%s\n%s\n%s\n", str, str, str)
str2 = fmt.Sprintf("%s%s", str2, str2)
// Verify we don't have overlapping reads at beginning of file
// 6 lines, 1206 characters. Should come back with seek position 0
seekTests = append(seekTests, seekTest{str2, 0})
for seg := 0; seg < 3; seg++ {
str = str + str
}
str2 = str + "\n"
fmt.Printf("str length is %d\n", len(str))
// str is 1,601 chars long now
for line := 0; line < 2; line++ {
str2 = str2 + str2
}
// str2 is 4 lines long. Should seek to beginning of file
seekTests = append(seekTests, seekTest{str2, 0})
// str2 is now 12 lines long. Should seek to position 6,404
str2 = fmt.Sprintf("%s%s%s", str2, str2, str2)
seekTests = append(seekTests, seekTest{str2, 6404})
td := os.TempDir()
for i, st := range seekTests {
log.Printf("Test case #%d: ", i)
testSeek(t, st, td)
}
}
func TestVersionReaderOpen(t *testing.T) {
version.Version = "1.2.1"
version.BuildNumber = "12345"
version.GitCommit = "abcdefg"
var vReader versionReader
en, err := vReader.open()
assert.NoError(t, err)
buf := new(bytes.Buffer)
buf.ReadFrom(en)
fullVersion := buf.String()
versionFields := strings.SplitN(fullVersion, "-", 3)
assert.Equal(t, len(versionFields), 3)
assert.Equal(t, versionFields[0], version.Version)
assert.Equal(t, versionFields[1], version.BuildNumber)
assert.Equal(t, versionFields[2], version.GitCommit)
}