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:
1
vendor/github.com/vmware/vic/cmd/vicadmin/html
generated
vendored
Symbolic link
1
vendor/github.com/vmware/vic/cmd/vicadmin/html
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../isos/vicadmin
|
||||
530
vendor/github.com/vmware/vic/cmd/vicadmin/logs.go
generated
vendored
Normal file
530
vendor/github.com/vmware/vic/cmd/vicadmin/logs.go
generated
vendored
Normal 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
575
vendor/github.com/vmware/vic/cmd/vicadmin/server.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
133
vendor/github.com/vmware/vic/cmd/vicadmin/usersession.go
generated
vendored
Normal file
133
vendor/github.com/vmware/vic/cmd/vicadmin/usersession.go
generated
vendored
Normal 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
634
vendor/github.com/vmware/vic/cmd/vicadmin/vicadm.go
generated
vendored
Normal 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()
|
||||
}
|
||||
327
vendor/github.com/vmware/vic/cmd/vicadmin/vicadm_test.go
generated
vendored
Normal file
327
vendor/github.com/vmware/vic/cmd/vicadmin/vicadm_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user