Merge branch 'master' of github.com:hashicorp/nomad

This commit is contained in:
Diptanu Choudhury 2016-03-21 17:45:13 -07:00
commit 4a1797e2c6
38 changed files with 793 additions and 146 deletions

View file

@ -2,7 +2,14 @@
IMPROVEMENTS:
* core: Allow count zero task groups to enable blue/green deploys [GH-931]
* cli: `alloc-status` display is less verbose by default [GH-946]
* cli: `server-members` displays the current leader in each region [GH-935]
* cli: New `inspect` command to display a submitted job's specification
[GH-952]
* cli: `node-status` display is less verbose by default and shows a node's
total resources [GH-946]
* client: `artifact` block now supports downloading paths relative to the
task's directory [GH-944]
## 0.3.1

3
Vagrantfile vendored
View file

@ -57,6 +57,9 @@ sudo apt-get install -y docker-engine
# Restart docker to make sure we get the latest version of the daemon if there is an upgrade
sudo service docker restart
# Install rkt
bash /opt/gopath/src/github.com/hashicorp/nomad/scripts/install_rkt.sh
# Make sure we can actually use docker as the vagrant user
sudo usermod -aG docker vagrant

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
@ -34,6 +35,9 @@ type QueryOptions struct {
// If set, used as prefix for resource list searches
Prefix string
// If set, pretty print the response json.
Pretty bool
}
// WriteOptions are used to parameterize a write
@ -156,6 +160,9 @@ func (r *request) setQueryOptions(q *QueryOptions) {
if q.Prefix != "" {
r.params.Set("prefix", q.Prefix)
}
if q.Pretty {
r.params.Set("pretty", "true")
}
}
// durToMsec converts a duration to a millisecond specified string
@ -265,6 +272,29 @@ func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*Quer
return qm, nil
}
// rawQuery is used to do a GET request against an endpoint and return the raw
// string result.
func (c *Client) rawQuery(endpoint string, q *QueryOptions) (string, *QueryMeta, error) {
r := c.newRequest("GET", endpoint)
r.setQueryOptions(q)
rtt, resp, err := requireOK(c.doRequest(r))
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
return string(raw), qm, nil
}
// write is used to do a PUT request against an endpoint
// and serialize/deserialized using the standard Nomad conventions.
func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {

View file

@ -63,6 +63,20 @@ func (j *Jobs) Info(jobID string, q *QueryOptions) (*Job, *QueryMeta, error) {
return &resp, qm, nil
}
// RawJob is used to retrieve information about a particular
// job given its unique ID and return the raw json.
func (j *Jobs) RawJob(jobID string, q *QueryOptions) (string, *QueryMeta, error) {
if q == nil {
q = &QueryOptions{}
}
q.Pretty = true
raw, qm, err := j.client.rawQuery("/v1/job/"+jobID, q)
if err != nil {
return "", nil, err
}
return raw, qm, nil
}
// Allocations is used to return the allocs for a given job ID.
func (j *Jobs) Allocations(jobID string, q *QueryOptions) ([]*AllocationListStub, *QueryMeta, error) {
var resp []*AllocationListStub

View file

@ -83,6 +83,39 @@ func TestJobs_Info(t *testing.T) {
}
}
func TestJobs_RawJob(t *testing.T) {
c, s := makeClient(t, nil, nil)
defer s.Stop()
jobs := c.Jobs()
// Trying to retrieve a job by ID before it exists
// returns an error
_, _, err := jobs.RawJob("job1", nil)
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("expected not found error, got: %#v", err)
}
// Register the job
job := testJob()
_, wm, err := jobs.Register(job, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
assertWriteMeta(t, wm)
// Query the job again and ensure it exists
result, qm, err := jobs.RawJob("job1", nil)
if err != nil {
t.Fatalf("err: %s", err)
}
assertQueryMeta(t, qm)
// Check that the result is what we expect
if result == "" || !strings.Contains(result, job.ID) {
t.Fatalf("expect: %#v, got: %#v", job, result)
}
}
func TestJobs_PrefixList(t *testing.T) {
c, s := makeClient(t, nil, nil)
defer s.Stop()

View file

@ -99,6 +99,7 @@ type Task struct {
type TaskArtifact struct {
GetterSource string
GetterOptions map[string]string
RelativeDest string
}
// NewTask creates and initializes a new Task.

View file

@ -113,7 +113,7 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
}, executorCtx)
if err != nil {
pluginClient.Kill()
return nil, fmt.Errorf("error starting process via the plugin: %v", err)
return nil, err
}
d.logger.Printf("[DEBUG] driver.exec: started process via plugin with pid: %v", ps.Pid)

View file

@ -7,6 +7,7 @@ import (
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
@ -159,20 +160,36 @@ func (e *UniversalExecutor) LaunchCmd(command *ExecCommand, ctx *ExecutorContext
e.cmd.Stdout = e.lro
e.cmd.Stderr = e.lre
// setting the env, path and args for the command
e.ctx.TaskEnv.Build()
e.cmd.Env = ctx.TaskEnv.EnvList()
e.cmd.Path = ctx.TaskEnv.ReplaceEnv(command.Cmd)
e.cmd.Args = append([]string{e.cmd.Path}, ctx.TaskEnv.ParseAndReplace(command.Args)...)
// Ensure that the binary being started is executable.
if err := e.makeExecutable(e.cmd.Path); err != nil {
// Look up the binary path and make it executable
absPath, err := e.lookupBin(ctx.TaskEnv.ReplaceEnv(command.Cmd))
if err != nil {
return nil, err
}
// starting the process
if err := e.makeExecutable(absPath); err != nil {
return nil, err
}
// Determine the path to run as it may have to be relative to the chroot.
path := absPath
if e.command.FSIsolation {
rel, err := filepath.Rel(e.taskDir, absPath)
if err != nil {
return nil, err
}
path = rel
}
// Set the commands arguments
e.cmd.Path = path
e.cmd.Args = append([]string{path}, ctx.TaskEnv.ParseAndReplace(command.Args)...)
e.cmd.Env = ctx.TaskEnv.EnvList()
// Start the process
if err := e.cmd.Start(); err != nil {
return nil, fmt.Errorf("error starting command: %v", err)
return nil, err
}
go e.wait()
ic := &cstructs.IsolationConfig{Cgroup: e.groups}
@ -328,8 +345,36 @@ func (e *UniversalExecutor) configureTaskDir() error {
return nil
}
// makeExecutablePosix makes the given file executable for root,group,others.
func (e *UniversalExecutor) makeExecutablePosix(binPath string) error {
// lookupBin looks for path to the binary to run by looking for the binary in
// the following locations, in-order: task/local/, task/, based on host $PATH.
// The return path is absolute.
func (e *UniversalExecutor) lookupBin(bin string) (string, error) {
// Check in the local directory
local := filepath.Join(e.taskDir, allocdir.TaskLocal, bin)
if _, err := os.Stat(local); err == nil {
return local, nil
}
// Check at the root of the task's directory
root := filepath.Join(e.taskDir, bin)
if _, err := os.Stat(root); err == nil {
return root, nil
}
// Check the $PATH
if host, err := exec.LookPath(bin); err == nil {
return host, nil
}
return "", fmt.Errorf("binary %q could not be found", bin)
}
// makeExecutable makes the given file executable for root,group,others.
func (e *UniversalExecutor) makeExecutable(binPath string) error {
if runtime.GOOS == "windows" {
return nil
}
fi, err := os.Stat(binPath)
if err != nil {
if os.IsNotExist(err) {

View file

@ -2,25 +2,7 @@
package executor
import (
"path/filepath"
"runtime"
cgroupConfig "github.com/opencontainers/runc/libcontainer/configs"
)
func (e *UniversalExecutor) makeExecutable(binPath string) error {
if runtime.GOOS == "windows" {
return nil
}
path := binPath
if !filepath.IsAbs(binPath) {
// The path must be relative the allocations directory.
path = filepath.Join(e.taskDir, binPath)
}
return e.makeExecutablePosix(path)
}
import cgroupConfig "github.com/opencontainers/runc/libcontainer/configs"
func (e *UniversalExecutor) configureChroot() error {
return nil

View file

@ -36,18 +36,6 @@ var (
}
)
func (e *UniversalExecutor) makeExecutable(binPath string) error {
path := binPath
if e.command.FSIsolation {
// The path must be relative the chroot
path = filepath.Join(e.taskDir, binPath)
} else if !filepath.IsAbs(binPath) {
// The path must be relative the allocations directory.
path = filepath.Join(e.taskDir, binPath)
}
return e.makeExecutablePosix(path)
}
// configureIsolation configures chroot and creates cgroups
func (e *UniversalExecutor) configureIsolation() error {
if e.command.FSIsolation {

View file

@ -11,11 +11,11 @@ import (
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/driver/env"
cstructs "github.com/hashicorp/nomad/client/driver/structs"
"github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
tu "github.com/hashicorp/nomad/testutil"
cstructs "github.com/hashicorp/nomad/client/driver/structs"
)
var (
@ -232,3 +232,38 @@ func TestExecutor_Start_Kill(t *testing.T) {
t.Fatalf("Command output incorrectly: want %v; got %v", expected, act)
}
}
func TestExecutor_MakeExecutable(t *testing.T) {
// Create a temp file
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer f.Close()
defer os.Remove(f.Name())
// Set its permissions to be non-executable
f.Chmod(os.FileMode(0610))
// Make a fake exececutor
ctx := testExecutorContext(t)
defer ctx.AllocDir.Destroy()
executor := NewExecutor(log.New(os.Stdout, "", log.LstdFlags))
err = executor.(*UniversalExecutor).makeExecutable(f.Name())
if err != nil {
t.Fatalf("makeExecutable() failed: %v", err)
}
// Check the permissions
stat, err := f.Stat()
if err != nil {
t.Fatalf("Stat() failed: %v", err)
}
act := stat.Mode().Perm()
exp := os.FileMode(0755)
if act != exp {
t.Fatalf("expected permissions %v; got %v", err)
}
}

View file

@ -173,7 +173,7 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
}, executorCtx)
if err != nil {
pluginClient.Kill()
return nil, fmt.Errorf("error starting process via the plugin: %v", err)
return nil, err
}
d.logger.Printf("[DEBUG] driver.java: started process with pid: %v", ps.Pid)

View file

@ -198,7 +198,7 @@ func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
ps, err := exec.LaunchCmd(&executor.ExecCommand{Cmd: args[0], Args: args[1:]}, executorCtx)
if err != nil {
pluginClient.Kill()
return nil, fmt.Errorf("error starting process via the plugin: %v", err)
return nil, err
}
d.logger.Printf("[INFO] Started new QemuVM: %s", vmID)

View file

@ -103,7 +103,7 @@ func (d *RawExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandl
ps, err := exec.LaunchCmd(&executor.ExecCommand{Cmd: command, Args: driverConfig.Args}, executorCtx)
if err != nil {
pluginClient.Kill()
return nil, fmt.Errorf("error starting process via the plugin: %v", err)
return nil, err
}
d.logger.Printf("[DEBUG] driver.raw_exec: started process with pid: %v", ps.Pid)

View file

@ -246,7 +246,7 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e
ps, err := execIntf.LaunchCmd(&executor.ExecCommand{Cmd: absPath, Args: cmdArgs}, executorCtx)
if err != nil {
pluginClient.Kill()
return nil, fmt.Errorf("error starting process via the plugin: %v", err)
return nil, err
}
d.logger.Printf("[DEBUG] driver.rkt: started ACI %q with: %v", img, cmdArgs)

View file

@ -4,6 +4,7 @@ import (
"fmt"
"log"
"net/url"
"path/filepath"
"sync"
gg "github.com/hashicorp/go-getter"
@ -59,16 +60,17 @@ func getGetterUrl(artifact *structs.TaskArtifact) (string, error) {
return u.String(), nil
}
// GetArtifact downloads an artifact into the specified destination directory.
func GetArtifact(artifact *structs.TaskArtifact, destDir string, logger *log.Logger) error {
// GetArtifact downloads an artifact into the specified task directory.
func GetArtifact(artifact *structs.TaskArtifact, taskDir string, logger *log.Logger) error {
url, err := getGetterUrl(artifact)
if err != nil {
return err
}
// Download the artifact
if err := getClient(url, destDir).Get(); err != nil {
return err
dest := filepath.Join(taskDir, artifact.RelativeDest)
if err := getClient(url, dest).Get(); err != nil {
return fmt.Errorf("GET error: %v", err)
}
return nil

View file

@ -21,11 +21,11 @@ func TestGetArtifact_FileAndChecksum(t *testing.T) {
defer ts.Close()
// Create a temp directory to download into
destDir, err := ioutil.TempDir("", "nomad-test")
taskDir, err := ioutil.TempDir("", "nomad-test")
if err != nil {
t.Fatalf("failed to make temp directory: %v", err)
}
defer os.RemoveAll(destDir)
defer os.RemoveAll(taskDir)
// Create the artifact
file := "test.sh"
@ -38,13 +38,48 @@ func TestGetArtifact_FileAndChecksum(t *testing.T) {
// Download the artifact
logger := log.New(os.Stderr, "", log.LstdFlags)
if err := GetArtifact(artifact, destDir, logger); err != nil {
if err := GetArtifact(artifact, taskDir, logger); err != nil {
t.Fatalf("GetArtifact failed: %v", err)
}
// Verify artifact exists
if _, err := os.Stat(filepath.Join(destDir, file)); err != nil {
t.Fatalf("source path error: %s", err)
if _, err := os.Stat(filepath.Join(taskDir, file)); err != nil {
t.Fatalf("file not found: %s", err)
}
}
func TestGetArtifact_File_RelativeDest(t *testing.T) {
// Create the test server hosting the file to download
ts := httptest.NewServer(http.FileServer(http.Dir(filepath.Dir("./test-fixtures/"))))
defer ts.Close()
// Create a temp directory to download into
taskDir, err := ioutil.TempDir("", "nomad-test")
if err != nil {
t.Fatalf("failed to make temp directory: %v", err)
}
defer os.RemoveAll(taskDir)
// Create the artifact
file := "test.sh"
relative := "foo/"
artifact := &structs.TaskArtifact{
GetterSource: fmt.Sprintf("%s/%s", ts.URL, file),
GetterOptions: map[string]string{
"checksum": "md5:bce963762aa2dbfed13caf492a45fb72",
},
RelativeDest: relative,
}
// Download the artifact
logger := log.New(os.Stderr, "", log.LstdFlags)
if err := GetArtifact(artifact, taskDir, logger); err != nil {
t.Fatalf("GetArtifact failed: %v", err)
}
// Verify artifact was downloaded to the correct path
if _, err := os.Stat(filepath.Join(taskDir, relative, file)); err != nil {
t.Fatalf("file not found: %s", err)
}
}
@ -54,11 +89,11 @@ func TestGetArtifact_InvalidChecksum(t *testing.T) {
defer ts.Close()
// Create a temp directory to download into
destDir, err := ioutil.TempDir("", "nomad-test")
taskDir, err := ioutil.TempDir("", "nomad-test")
if err != nil {
t.Fatalf("failed to make temp directory: %v", err)
}
defer os.RemoveAll(destDir)
defer os.RemoveAll(taskDir)
// Create the artifact with an incorrect checksum
file := "test.sh"
@ -71,7 +106,7 @@ func TestGetArtifact_InvalidChecksum(t *testing.T) {
// Download the artifact and expect an error
logger := log.New(os.Stderr, "", log.LstdFlags)
if err := GetArtifact(artifact, destDir, logger); err == nil {
if err := GetArtifact(artifact, taskDir, logger); err == nil {
t.Fatalf("GetArtifact should have failed")
}
}
@ -116,17 +151,17 @@ func TestGetArtifact_Archive(t *testing.T) {
// Create a temp directory to download into and create some of the same
// files that exist in the artifact to ensure they are overriden
destDir, err := ioutil.TempDir("", "nomad-test")
taskDir, err := ioutil.TempDir("", "nomad-test")
if err != nil {
t.Fatalf("failed to make temp directory: %v", err)
}
defer os.RemoveAll(destDir)
defer os.RemoveAll(taskDir)
create := map[string]string{
"exist/my.config": "to be replaced",
"untouched": "existing top-level",
}
createContents(destDir, create, t)
createContents(taskDir, create, t)
file := "archive.tar.gz"
artifact := &structs.TaskArtifact{
@ -137,7 +172,7 @@ func TestGetArtifact_Archive(t *testing.T) {
}
logger := log.New(os.Stderr, "", log.LstdFlags)
if err := GetArtifact(artifact, destDir, logger); err != nil {
if err := GetArtifact(artifact, taskDir, logger); err != nil {
t.Fatalf("GetArtifact failed: %v", err)
}
@ -148,5 +183,5 @@ func TestGetArtifact_Archive(t *testing.T) {
"new/my.config": "hello world\n",
"test.sh": "sleep 1\n",
}
checkContents(destDir, expected, t)
checkContents(taskDir, expected, t)
}

View file

@ -240,7 +240,18 @@ func (r *TaskRunner) run() {
return
}
for _, artifact := range r.task.Artifacts {
for i, artifact := range r.task.Artifacts {
// Verify the artifact doesn't escape the task directory.
if err := artifact.Validate(); err != nil {
// If this error occurs there is potentially a server bug or
// mallicious, server spoofing.
r.setState(structs.TaskStateDead,
structs.NewTaskEvent(structs.TaskArtifactDownloadFailed).SetDownloadError(err))
r.logger.Printf("[ERR] client: allocation %q, task %v, artifact %v (%v) fails validation",
r.alloc.ID, r.task.Name, artifact, i)
return
}
if err := getter.GetArtifact(artifact, taskDir, r.logger); err != nil {
r.setState(structs.TaskStateDead,
structs.NewTaskEvent(structs.TaskArtifactDownloadFailed).SetDownloadError(err))

View file

@ -127,9 +127,29 @@ func (a *Agent) serverConfig() (*nomad.Config, error) {
}
if addr := a.config.Addresses.RPC; addr != "" {
conf.RPCAddr.IP = net.ParseIP(addr)
} else if device := a.config.Interfaces.RPC; device != "" {
ip, err := ipOfDevice(device)
if err != nil {
return nil, err
}
conf.RPCAddr.IP = ip
}
if addr := a.config.Addresses.Serf; addr != "" {
conf.SerfConfig.MemberlistConfig.BindAddr = addr
} else if device := a.config.Interfaces.Serf; device != "" {
ip, err := ipOfDevice(device)
if err != nil {
return nil, err
}
conf.SerfConfig.MemberlistConfig.BindAddr = ip.String()
}
if device := a.config.Interfaces.HTTP; device != "" && a.config.Addresses.HTTP == "" {
ip, err := ipOfDevice(device)
if err != nil {
return nil, err
}
a.config.Addresses.HTTP = ip.String()
}
// Set up the ports
@ -209,12 +229,21 @@ func (a *Agent) clientConfig() (*clientconfig.Config, error) {
conf.Node.Name = a.config.NodeName
conf.Node.Meta = a.config.Client.Meta
conf.Node.NodeClass = a.config.Client.NodeClass
// Setting the proper HTTP Addr
httpAddr := fmt.Sprintf("%s:%d", a.config.BindAddr, a.config.Ports.HTTP)
if a.config.Addresses.HTTP != "" && a.config.AdvertiseAddrs.HTTP == "" {
if a.config.Addresses.HTTP != "" && a.config.AdvertiseAddrs.HTTP == "" && a.config.Interfaces.HTTP == "" {
httpAddr = fmt.Sprintf("%s:%d", a.config.Addresses.HTTP, a.config.Ports.HTTP)
if _, err := net.ResolveTCPAddr("tcp", httpAddr); err != nil {
return nil, fmt.Errorf("error resolving http addr: %v:", err)
}
} else if a.config.Interfaces.HTTP != "" && a.config.AdvertiseAddrs.HTTP == "" {
ip, err := ipOfDevice(a.config.Interfaces.HTTP)
if err != nil {
return nil, fmt.Errorf("error finding ip address from interface %q: %v", a.config.Interfaces.HTTP, err)
}
a.config.Addresses.HTTP = ip.String()
httpAddr = fmt.Sprintf("%s:%d", ip.String(), a.config.Ports.HTTP)
} else if a.config.AdvertiseAddrs.HTTP != "" {
addr, err := net.ResolveTCPAddr("tcp", a.config.AdvertiseAddrs.HTTP)
if err != nil {
@ -223,7 +252,6 @@ func (a *Agent) clientConfig() (*clientconfig.Config, error) {
httpAddr = fmt.Sprintf("%s:%d", addr.IP.String(), addr.Port)
}
conf.Node.HTTPAddr = httpAddr
conf.Version = a.config.Version
// Reserve resources on the node.
r := conf.Node.Reserved
@ -237,6 +265,8 @@ func (a *Agent) clientConfig() (*clientconfig.Config, error) {
r.IOPS = a.config.Client.Reserved.IOPS
conf.GloballyReservedPorts = a.config.Client.Reserved.ParsedReservedPorts
conf.Version = a.config.Version
return conf, nil
}

View file

@ -46,6 +46,10 @@ type Config struct {
// Addresses is used to override the network addresses we bind to.
Addresses *Addresses `mapstructure:"addresses"`
// Interfaces is used to override the network addresses we bind to by
// providing device names
Interfaces *Interfaces `mapstructure:"interfaces"`
// AdvertiseAddrs is used to control the addresses we advertise.
AdvertiseAddrs *AdvertiseAddrs `mapstructure:"advertise"`
@ -255,6 +259,14 @@ type Addresses struct {
Serf string `mapstructure:"serf"`
}
// Interfaces provides an alternative to the Addresses configuration. We pick an
// ip configured on the devide specified and use that to bind.
type Interfaces struct {
HTTP string `mapstructure:"http"`
RPC string `mapstructure:"rpc"`
Serf string `mapstructure:"serf"`
}
// AdvertiseAddrs is used to control the addresses we advertise out for
// different network services. Not all network services support an
// advertise address. All are optional and default to BindAddr.
@ -366,6 +378,7 @@ func DefaultConfig() *Config {
Serf: 4648,
},
Addresses: &Addresses{},
Interfaces: &Interfaces{},
AdvertiseAddrs: &AdvertiseAddrs{},
Atlas: &AtlasConfig{},
Client: &ClientConfig{
@ -496,6 +509,14 @@ func (c *Config) Merge(b *Config) *Config {
result.Addresses = result.Addresses.Merge(b.Addresses)
}
// Apply the interfaces config
if result.Interfaces == nil && b.Interfaces != nil {
interfaces := *b.Interfaces
result.Interfaces = &interfaces
} else if b.Interfaces != nil {
result.Interfaces = result.Interfaces.Merge(b.Interfaces)
}
// Apply the advertise addrs config
if result.AdvertiseAddrs == nil && b.AdvertiseAddrs != nil {
advertise := *b.AdvertiseAddrs
@ -675,6 +696,22 @@ func (a *Addresses) Merge(b *Addresses) *Addresses {
return &result
}
// Merge is used to merge two interfaces configs together.
func (i *Interfaces) Merge(b *Interfaces) *Interfaces {
result := *i
if b.HTTP != "" {
result.HTTP = b.HTTP
}
if b.RPC != "" {
result.RPC = b.RPC
}
if b.Serf != "" {
result.Serf = b.Serf
}
return &result
}
// Merge merges two advertise addrs configs together.
func (a *AdvertiseAddrs) Merge(b *AdvertiseAddrs) *AdvertiseAddrs {
result := *a

View file

@ -78,6 +78,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
"enable_debug",
"ports",
"addresses",
"interfaces",
"advertise",
"client",
"server",
@ -102,6 +103,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
}
delete(m, "ports")
delete(m, "addresses")
delete(m, "interfaces")
delete(m, "advertise")
delete(m, "client")
delete(m, "server")
@ -128,6 +130,13 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
}
}
// Parse interfaces
if o := list.Filter("interfaces"); len(o.Items) > 0 {
if err := parseInterfaces(&result.Interfaces, o); err != nil {
return multierror.Prefix(err, "interfaces ->")
}
}
// Parse advertise
if o := list.Filter("advertise"); len(o.Items) > 0 {
if err := parseAdvertise(&result.AdvertiseAddrs, o); err != nil {
@ -244,6 +253,38 @@ func parseAddresses(result **Addresses, list *ast.ObjectList) error {
return nil
}
func parseInterfaces(result **Interfaces, list *ast.ObjectList) error {
list = list.Elem()
if len(list.Items) > 1 {
return fmt.Errorf("only one 'interfaces' block allowed")
}
// Get our interfaces object
listVal := list.Items[0].Val
// Check for the invalid keys
valid := []string{
"http",
"rpc",
"serf",
}
if err := checkHCLKeys(listVal, valid); err != nil {
return err
}
var m map[string]interface{}
if err := hcl.DecodeObject(&m, listVal); err != nil {
return err
}
var interfaces Interfaces
if err := mapstructure.WeakDecode(m, &interfaces); err != nil {
return err
}
*result = &interfaces
return nil
}
func parseAdvertise(result **AdvertiseAddrs, list *ast.ObjectList) error {
list = list.Elem()
if len(list.Items) > 1 {

View file

@ -1,7 +1,9 @@
package agent
import (
"fmt"
"math/rand"
"net"
"time"
)
@ -9,3 +11,43 @@ import (
func randomStagger(intv time.Duration) time.Duration {
return time.Duration(uint64(rand.Int63()) % uint64(intv))
}
// IpOfDevice returns a routable ip addr of a device
func ipOfDevice(name string) (net.IP, error) {
intf, err := net.InterfaceByName(name)
if err != nil {
return nil, err
}
addrs, err := intf.Addrs()
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no ips were detected on the interface: %v", name)
}
// Iterating through the IPs configured for that device and returning the
// the first ipv4 address configured. If no ipv4 addresses are configured,
// we return the first ipv6 addr if any ipv6 addr is configured.
var ipv6Addrs []net.IP
for _, addr := range addrs {
var ip net.IP
switch v := (addr).(type) {
case *net.IPNet:
ip = v.IP
if ip.To4() != nil {
return ip, nil
}
if ip.To16() != nil {
ipv6Addrs = append(ipv6Addrs, ip)
continue
}
case *net.IPAddr:
continue
}
}
if len(ipv6Addrs) > 0 {
return ipv6Addrs[0], nil
}
return nil, fmt.Errorf("no ips were detected on the interface: %v", name)
}

View file

@ -126,14 +126,22 @@ func (c *AllocStatusCommand) Run(args []string) int {
fmt.Sprintf("Node ID|%s", limit(alloc.NodeID, length)),
fmt.Sprintf("Job ID|%s", alloc.JobID),
fmt.Sprintf("Client Status|%s", alloc.ClientStatus),
fmt.Sprintf("Evaluated Nodes|%d", alloc.Metrics.NodesEvaluated),
fmt.Sprintf("Filtered Nodes|%d", alloc.Metrics.NodesFiltered),
fmt.Sprintf("Exhausted Nodes|%d", alloc.Metrics.NodesExhausted),
fmt.Sprintf("Allocation Time|%s", alloc.Metrics.AllocationTime),
fmt.Sprintf("Failures|%d", alloc.Metrics.CoalescedFailures),
}
if verbose {
basic = append(basic,
fmt.Sprintf("Evaluated Nodes|%d", alloc.Metrics.NodesEvaluated),
fmt.Sprintf("Filtered Nodes|%d", alloc.Metrics.NodesFiltered),
fmt.Sprintf("Exhausted Nodes|%d", alloc.Metrics.NodesExhausted),
fmt.Sprintf("Allocation Time|%s", alloc.Metrics.AllocationTime),
fmt.Sprintf("Failures|%d", alloc.Metrics.CoalescedFailures))
}
c.Ui.Output(formatKV(basic))
if !short {
c.taskResources(alloc)
}
// Print the state of each task.
if short {
c.shortTaskStatus(alloc)
@ -142,12 +150,9 @@ func (c *AllocStatusCommand) Run(args []string) int {
}
// Format the detailed status
c.Ui.Output("\n==> Status")
dumpAllocStatus(c.Ui, alloc, length)
if !short {
c.Ui.Output("\n==> Task Resources")
c.taskResources(alloc)
if verbose || alloc.DesiredStatus == "failed" {
c.Ui.Output("\n==> Status")
dumpAllocStatus(c.Ui, alloc, length)
}
return 0
@ -283,6 +288,11 @@ func (c *AllocStatusCommand) allocResources(alloc *api.Allocation) {
// taskResources prints out the tasks current resource usage
func (c *AllocStatusCommand) taskResources(alloc *api.Allocation) {
if len(alloc.TaskResources) == 0 {
return
}
c.Ui.Output("\n==> Task Resources")
firstLine := true
for task, resource := range alloc.TaskResources {
header := fmt.Sprintf("\nTask: %q", task)

86
command/inspect.go Normal file
View file

@ -0,0 +1,86 @@
package command
import (
"fmt"
"strings"
)
type InspectCommand struct {
Meta
}
func (c *InspectCommand) Help() string {
helpText := `
Usage: nomad inspect [options] <job>
Inspect is used to see the specification of a submitted job.
General Options:
` + generalOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *InspectCommand) Synopsis() string {
return "Inspect a submitted job"
}
func (c *InspectCommand) Run(args []string) int {
flags := c.Meta.FlagSet("inspect", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got exactly one job
args = flags.Args()
if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
jobID := args[0]
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Check if the job exists
jobs, _, err := client.Jobs().PrefixList(jobID)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
return 1
}
if len(jobs) == 0 {
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
return 1
}
if len(jobs) > 1 {
out := make([]string, len(jobs)+1)
out[0] = "ID|Type|Priority|Status"
for i, job := range jobs {
out[i+1] = fmt.Sprintf("%s|%s|%d|%s",
job.ID,
job.Type,
job.Priority,
job.Status)
}
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out)))
return 0
}
// Prefix lookup matched a single job
job, _, err := client.Jobs().RawJob(jobs[0].ID, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
return 1
}
// Print the contents of the job
c.Ui.Output(job)
return 0
}

46
command/inspect_test.go Normal file
View file

@ -0,0 +1,46 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestInspectCommand_Implements(t *testing.T) {
var _ cli.Command = &InspectCommand{}
}
func TestInspectCommand_Fails(t *testing.T) {
srv, _, url := testServer(t, nil)
defer srv.Stop()
ui := new(cli.MockUi)
cmd := &InspectCommand{Meta: Meta{Ui: ui}}
// Fails on misuse
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
t.Fatalf("expected help output, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on non-existent job ID
if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 {
t.Fatalf("expect exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No job(s) with prefix or id") {
t.Fatalf("expect not found error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on connection failure
if code := cmd.Run([]string{"-address=nope", "nope"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error inspecting job") {
t.Fatalf("expected failed query error, got: %s", out)
}
}

View file

@ -2,9 +2,10 @@ package command
import (
"fmt"
"github.com/hashicorp/nomad/api"
"sort"
"strings"
"github.com/hashicorp/nomad/api"
)
type NodeStatusCommand struct {
@ -179,20 +180,6 @@ func (c *NodeStatusCommand) Run(args []string) int {
return 1
}
m := node.Attributes
keys := make([]string, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
var attributes []string
for _, k := range keys {
if k != "" {
attributes = append(attributes, fmt.Sprintf("%s:%s", k, m[k]))
}
}
// Format the output
basic := []string{
fmt.Sprintf("ID|%s", limit(node.ID, length)),
@ -201,19 +188,10 @@ func (c *NodeStatusCommand) Run(args []string) int {
fmt.Sprintf("DC|%s", node.Datacenter),
fmt.Sprintf("Drain|%v", node.Drain),
fmt.Sprintf("Status|%s", node.Status),
fmt.Sprintf("Attributes|%s", strings.Join(attributes, ", ")),
}
// Dump the output
c.Ui.Output(formatKV(basic))
if !short {
allocs, err := getAllocs(client, node, length)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying node allocations: %s", err))
return 1
}
c.Ui.Output("\n==> Allocations")
c.Ui.Output(formatList(allocs))
resources, err := getResources(client, node)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying node resources: %s", err))
@ -221,7 +199,37 @@ func (c *NodeStatusCommand) Run(args []string) int {
}
c.Ui.Output("\n==> Resource Utilization")
c.Ui.Output(formatList(resources))
allocs, err := getAllocs(client, node, length)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying node allocations: %s", err))
return 1
}
if len(allocs) > 1 {
c.Ui.Output("\n==> Allocations")
c.Ui.Output(formatList(allocs))
}
}
if verbose {
// Print the attributes
keys := make([]string, len(node.Attributes))
for k := range node.Attributes {
keys = append(keys, k)
}
sort.Strings(keys)
var attributes []string
for _, k := range keys {
if k != "" {
attributes = append(attributes, fmt.Sprintf("%s|%s", k, node.Attributes[k]))
}
}
c.Ui.Output("\n==> Attributes")
c.Ui.Output(formatKV(attributes))
}
return 0
}
@ -260,9 +268,19 @@ func getAllocs(client *api.Client, node *api.Node, length int) ([]string, error)
return allocs, err
}
// getResources returns the resource usage of the node.
func getResources(client *api.Client, node *api.Node) ([]string, error) {
var resources []string
var cpu, mem, disk, iops int
var totalCpu, totalMem, totalDisk, totalIops int
// Compute the total
r := node.Resources
res := node.Reserved
totalCpu = r.CPU - res.CPU
totalMem = r.MemoryMB - res.MemoryMB
totalDisk = r.DiskMB - res.DiskMB
totalIops = r.IOPS - res.IOPS
// Get list of running allocations on the node
runningAllocs, err := getRunningAllocs(client, node.ID)
@ -277,11 +295,15 @@ func getResources(client *api.Client, node *api.Node) ([]string, error) {
resources = make([]string, 2)
resources[0] = "CPU|Memory MB|Disk MB|IOPS"
resources[1] = fmt.Sprintf("%v|%v|%v|%v",
resources[1] = fmt.Sprintf("%v/%v|%v/%v|%v/%v|%v/%v",
cpu,
totalCpu,
mem,
totalMem,
disk,
iops)
totalDisk,
iops,
totalIops)
return resources, err
}

View file

@ -58,9 +58,6 @@ func TestNodeStatusCommand_Run(t *testing.T) {
if !strings.Contains(out, "mynode") {
t.Fatalf("expect to find mynode, got: %s", out)
}
if !strings.Contains(out, "Allocations") {
t.Fatalf("expected allocations, got: %s", out)
}
ui.OutputWriter.Reset()
// Query single node in short view
@ -83,9 +80,6 @@ func TestNodeStatusCommand_Run(t *testing.T) {
if !strings.Contains(out, "mynode") {
t.Fatalf("expect to find mynode, got: %s", out)
}
if !strings.Contains(out, "Allocations") {
t.Fatalf("expected allocations, got: %s", out)
}
if strings.Contains(out, nodeID) {
t.Fatalf("expected truncated node id, got: %s", out)
}

View file

@ -75,36 +75,33 @@ func (c *StopCommand) Run(args []string) int {
}
// Check if the job exists
job, _, err := client.Jobs().Info(jobID, nil)
jobs, _, err := client.Jobs().PrefixList(jobID)
if err != nil {
jobs, _, err := client.Jobs().PrefixList(jobID)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err))
return 1
}
if len(jobs) == 0 {
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
return 1
}
if len(jobs) > 1 {
out := make([]string, len(jobs)+1)
out[0] = "ID|Type|Priority|Status"
for i, job := range jobs {
out[i+1] = fmt.Sprintf("%s|%s|%d|%s",
job.ID,
job.Type,
job.Priority,
job.Status)
}
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out)))
return 0
}
// Prefix lookup matched a single job
job, _, err = client.Jobs().Info(jobs[0].ID, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err))
return 1
c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err))
return 1
}
if len(jobs) == 0 {
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
return 1
}
if len(jobs) > 1 {
out := make([]string, len(jobs)+1)
out[0] = "ID|Type|Priority|Status"
for i, job := range jobs {
out[i+1] = fmt.Sprintf("%s|%s|%d|%s",
job.ID,
job.Type,
job.Priority,
job.Status)
}
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out)))
return 0
}
// Prefix lookup matched a single job
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err))
return 1
}
// Invoke the stop

View file

@ -87,7 +87,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"inspect": func() (cli.Command, error) {
return &command.InspectCommand{
Meta: meta,
}, nil
},
"node-drain": func() (cli.Command, error) {
return &command.NodeDrainCommand{
Meta: meta,

View file

@ -617,6 +617,7 @@ func parseArtifacts(result *[]*structs.TaskArtifact, list *ast.ObjectList) error
valid := []string{
"source",
"options",
"destination",
}
if err := checkHCLKeys(o.Val, valid); err != nil {
return err
@ -629,6 +630,11 @@ func parseArtifacts(result *[]*structs.TaskArtifact, list *ast.ObjectList) error
delete(m, "options")
// Default to downloading to the local directory.
if _, ok := m["destination"]; !ok {
m["destination"] = "local/"
}
var ta structs.TaskArtifact
if err := mapstructure.WeakDecode(m, &ta); err != nil {
return err

View file

@ -131,12 +131,14 @@ func TestParse(t *testing.T) {
Artifacts: []*structs.TaskArtifact{
{
GetterSource: "http://foo.com/artifact",
RelativeDest: "local/",
GetterOptions: map[string]string{
"checksum": "md5:b8a4f3f72ecab0510a6a31e997461c5f",
},
},
{
GetterSource: "http://bar.com/artifact",
RelativeDest: "local/",
GetterOptions: map[string]string{
"checksum": "md5:ff1cc0d3432dad54d607c1505fb7245c",
},
@ -320,6 +322,58 @@ func TestParse(t *testing.T) {
nil,
true,
},
{
"artifacts.hcl",
&structs.Job{
ID: "binstore-storagelocker",
Name: "binstore-storagelocker",
Type: "service",
Priority: 50,
Region: "global",
TaskGroups: []*structs.TaskGroup{
&structs.TaskGroup{
Name: "binsl",
Count: 1,
Tasks: []*structs.Task{
&structs.Task{
Name: "binstore",
Driver: "docker",
Resources: &structs.Resources{
CPU: 100,
MemoryMB: 10,
DiskMB: 300,
IOPS: 0,
},
LogConfig: &structs.LogConfig{
MaxFiles: 10,
MaxFileSizeMB: 10,
},
Artifacts: []*structs.TaskArtifact{
{
GetterSource: "http://foo.com/bar",
GetterOptions: map[string]string{},
RelativeDest: "",
},
{
GetterSource: "http://foo.com/baz",
GetterOptions: map[string]string{},
RelativeDest: "local/",
},
{
GetterSource: "http://foo.com/bam",
GetterOptions: map[string]string{},
RelativeDest: "var/foo",
},
},
},
},
},
},
},
false,
},
}
for _, tc := range cases {

View file

@ -0,0 +1,21 @@
job "binstore-storagelocker" {
group "binsl" {
task "binstore" {
driver = "docker"
artifact {
source = "http://foo.com/bar"
destination = ""
}
artifact {
source = "http://foo.com/baz"
}
artifact {
source = "http://foo.com/bam"
destination = "var/foo"
}
resources {}
}
}
}

View file

@ -34,6 +34,7 @@ func RunCustom(args []string, commands map[string]cli.CommandFactory) int {
switch k {
case "executor":
case "syslog":
case "fs ls", "fs cat", "fs stat":
default:
commandsInclude = append(commandsInclude, k)
}

View file

@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/url"
"path/filepath"
"reflect"
"regexp"
"strconv"
@ -1913,6 +1914,10 @@ type TaskArtifact struct {
// GetterOptions are options to use when downloading the artifact using
// go-getter.
GetterOptions map[string]string `mapstructure:"options"`
// RelativeDest is the download destination given relative to the task's
// directory.
RelativeDest string `mapstructure:"destination"`
}
func (ta *TaskArtifact) Copy() *TaskArtifact {
@ -1925,16 +1930,36 @@ func (ta *TaskArtifact) Copy() *TaskArtifact {
return nta
}
func (ta *TaskArtifact) GoString() string {
return fmt.Sprintf("%+v", ta)
}
func (ta *TaskArtifact) Validate() error {
// Verify the source
var mErr multierror.Error
if ta.GetterSource == "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("source must be specified"))
} else {
_, err := url.Parse(ta.GetterSource)
if err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid source URL %q: %v", ta.GetterSource, err))
}
}
_, err := url.Parse(ta.GetterSource)
// Verify the destination doesn't escape the tasks directory
alloc := "/foo/bar/"
abs, err := filepath.Abs(filepath.Join(alloc, ta.RelativeDest))
if err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid source URL %q: %v", ta.GetterSource, err))
mErr.Errors = append(mErr.Errors, err)
return mErr.ErrorOrNil()
}
rel, err := filepath.Rel(alloc, abs)
if err != nil {
mErr.Errors = append(mErr.Errors, err)
return mErr.ErrorOrNil()
}
if strings.HasPrefix(rel, "..") {
mErr.Errors = append(mErr.Errors, fmt.Errorf("destination escapes task's directory"))
}
// Verify the checksum

View file

@ -777,6 +777,28 @@ func TestTaskArtifact_Validate_Source(t *testing.T) {
}
}
func TestTaskArtifact_Validate_Dest(t *testing.T) {
valid := &TaskArtifact{GetterSource: "google.com"}
if err := valid.Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
valid.RelativeDest = "local/"
if err := valid.Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
valid.RelativeDest = "local/.."
if err := valid.Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
valid.RelativeDest = "local/../.."
if err := valid.Validate(); err == nil {
t.Fatalf("expected error: %v", err)
}
}
func TestTaskArtifact_Validate_Checksum(t *testing.T) {
cases := []struct {
Input *TaskArtifact

View file

@ -144,6 +144,22 @@ nodes, unless otherwise specified:
server nodes from the same datacenter if possible. Used only on server
nodes.
* <a id="interfaces">`interfaces`</a>: Provides an alternative to the
`addresses` configuration. Operators can provide network device names to which
Nomad binds individual network services. Nomad looks for the first IPv4
address configured for the device and uses it, and if no IPv4 address is
present then it looks for an IPv6 address. The value is a map of device names of
network interfaces and supports the following keys:
<br>
* `http`: The device name the HTTP server is bound to. Applies to both clients and servers.
* `rpc`: The device name to bind the internal RPC interfaces to. Should be exposed
only to other cluster members if possible. Used only on server nodes, but
must be accessible from all agents.
* `serf`: The device name used to bind the gossip layer to. Both a TCP and UDP
listener will be exposed on this address. Should be restricted to only
server nodes from the same datacenter if possible. Used only on server
nodes.
* `advertise`: Controls the advertise address for individual network services.
This can be used to advertise a different address to the peers of a server
node to support more complex network configurations such as NAT. This

View file

@ -430,6 +430,10 @@ The `artifact` object maps supports the following keys:
* `source` - The path to the artifact to download.
* `destination` - An optional path to download the artifact into relative to the
root of the task's directory. If the `destination` key is omitted, it will
default to `local/`.
* `options` - The `options` block allows setting parameters for `go-getter`. An
example is given below:

View file

@ -414,13 +414,16 @@ is started.
The `Artifact` object maps supports the following keys:
* `Source` - The path to the artifact to download.
* `GetterSource` - The path to the artifact to download.
* `Options` - The `options` block allows setting parameters for `go-getter`. An
example is given below:
* `RelativeDest` - The destination to download the artifact relative the task's
directory.
* `GetterOptions` - A `map[string]string` block of options for `go-getter`. An
example is given below:
```
"Options": {
"GetterOptions": {
"checksum": "md5:c4aa853ad2215426eb7d70a21922e794",
"aws_access_key_id": "<id>",