Beispiel #1
0
// units returns a map representing the current state of units known by the agent.
func (a *Agent) units() (unitStates, error) {
	launched := pkg.NewUnsafeSet()
	for _, jName := range a.cache.launchedJobs() {
		launched.Add(jName)
	}

	loaded := pkg.NewUnsafeSet()
	for _, jName := range a.cache.loadedJobs() {
		loaded.Add(jName)
	}

	units, err := a.um.Units()
	if err != nil {
		return nil, fmt.Errorf("failed fetching loaded units from UnitManager: %v", err)
	}

	filter := pkg.NewUnsafeSet()
	for _, u := range units {
		filter.Add(u)
	}

	states := make(unitStates)
	for _, uName := range units {
		js := job.JobStateInactive
		if loaded.Contains(uName) {
			js = job.JobStateLoaded
		} else if launched.Contains(uName) {
			js = job.JobStateLaunched
		}
		states[uName] = js
	}

	return states, nil
}
Beispiel #2
0
// ValidateOptions ensures that a set of UnitOptions is valid; if not, an error
// is returned detailing the issue encountered.  If there are several problems
// with a set of options, only the first is returned.
func ValidateOptions(opts []*schema.UnitOption) error {
	uf := schema.MapSchemaUnitOptionsToUnitFile(opts)
	// Sanity check using go-systemd's deserializer, which will do things
	// like check for excessive line lengths
	_, err := gsunit.Deserialize(gsunit.Serialize(uf.Options))
	if err != nil {
		return err
	}

	j := &job.Job{
		Unit: *uf,
	}
	conflicts := pkg.NewUnsafeSet(j.Conflicts()...)
	replaces := pkg.NewUnsafeSet(j.Replaces()...)
	peers := pkg.NewUnsafeSet(j.Peers()...)
	for _, peer := range peers.Values() {
		for _, conflict := range conflicts.Values() {
			matched, _ := path.Match(conflict, peer)
			if matched {
				return fmt.Errorf("unresolvable requirements: peer %q matches conflict %q", peer, conflict)
			}
		}
		for _, replace := range replaces.Values() {
			matched, _ := path.Match(replace, peer)
			if matched {
				return fmt.Errorf("unresolvable requirements: peer %q matches replace %q", peer, replace)
			}
		}
	}
	hasPeers := peers.Length() != 0
	hasConflicts := conflicts.Length() != 0
	hasReplaces := replaces.Length() != 0
	_, hasReqTarget := j.RequiredTarget()
	u := &job.Unit{
		Unit: *uf,
	}
	isGlobal := u.IsGlobal()

	switch {
	case hasReqTarget && hasPeers:
		return errors.New("MachineID cannot be used with Peers")
	case hasReqTarget && hasConflicts:
		return errors.New("MachineID cannot be used with Conflicts")
	case hasReqTarget && isGlobal:
		return errors.New("MachineID cannot be used with Global")
	case hasReqTarget && hasReplaces:
		return errors.New("MachineID cannot be used with Replaces")
	case isGlobal && hasPeers:
		return errors.New("Global cannot be used with Peers")
	case isGlobal && hasReplaces:
		return errors.New("Global cannot be used with Replaces")
	case hasConflicts && hasReplaces:
		return errors.New("Conflicts cannot be used with Replaces")
	}

	return nil
}
Beispiel #3
0
func TestHasMetadata(t *testing.T) {
	testCases := []struct {
		metadata map[string]string
		match    map[string]pkg.Set
		want     bool
	}{
		{
			map[string]string{
				"region": "us-east-1",
			},
			map[string]pkg.Set{
				"region": pkg.NewUnsafeSet("us-east-1"),
			},
			true,
		},
		{
			map[string]string{
				"groups": "ping",
			},
			map[string]pkg.Set{
				"groups": pkg.NewUnsafeSet("ping", "pong"),
			},
			true,
		},
		{
			map[string]string{
				"groups": "ping",
			},
			map[string]pkg.Set{
				"groups": pkg.NewUnsafeSet("pong"),
			},
			false,
		},
		{
			map[string]string{
				"region": "us-east-1",
				"groups": "ping",
			},
			map[string]pkg.Set{
				"region": pkg.NewUnsafeSet("us-east-1"),
				"groups": pkg.NewUnsafeSet("pong"),
			},
			false,
		},
	}

	for i, tt := range testCases {
		ms := &MachineState{Metadata: tt.metadata}
		got := HasMetadata(ms, tt.match)
		if got != tt.want {
			t.Errorf("case %d: HasMetadata returned %t, expected %t", i, got, tt.want)
		}
	}
}
Beispiel #4
0
// jobs returns a collection of all Jobs that the Agent has either loaded
// or launched. The Unit, TargetState and TargetMachineID fields of the
// returned *job.Job objects are not properly hydrated.
func (a *Agent) jobs() (map[string]*job.Job, error) {
	launched := pkg.NewUnsafeSet()
	for _, jName := range a.cache.launchedJobs() {
		launched.Add(jName)
	}

	loaded := pkg.NewUnsafeSet()
	for _, jName := range a.cache.loadedJobs() {
		loaded.Add(jName)
	}

	units, err := a.um.Units()
	if err != nil {
		return nil, fmt.Errorf("failed fetching loaded units from UnitManager: %v", err)
	}

	filter := pkg.NewUnsafeSet()
	for _, u := range units {
		filter.Add(u)
	}
	states, err := a.um.GetUnitStates(filter)
	if err != nil {
		return nil, fmt.Errorf("failed fetching unit states: %v", err)
	}

	jobs := make(map[string]*job.Job)
	for _, uName := range units {
		jobs[uName] = &job.Job{
			Name:      uName,
			UnitState: states[uName],
			State:     nil,

			// The following fields are not properly populated
			// and should not be used in the calling code
			Unit:            unit.Unit{},
			TargetState:     job.JobState(""),
			TargetMachineID: "",
		}

		js := job.JobStateInactive
		if loaded.Contains(uName) {
			js = job.JobStateLoaded
		} else if launched.Contains(uName) {
			js = job.JobStateLaunched
		}
		jobs[uName].State = &js
	}

	return jobs, nil
}
Beispiel #5
0
// calculateTasksForUnits compares the desired and current state of an Agent.
// The generated tasks represent what, in order, should be done to make the
//  desired state match the current state.
func (ar *AgentReconciler) calculateTasksForUnits(dState *AgentState, cState unitStates) []task {
	jobs := pkg.NewUnsafeSet()
	for cName := range cState {
		jobs.Add(cName)
	}

	for dName := range dState.Units {
		jobs.Add(dName)
	}

	sorted := sort.StringSlice(jobs.Values())
	sorted.Sort()

	var tasks []task
	for _, name := range sorted {
		tasks = append(tasks, ar.calculateTasksForUnit(dState, cState, name)...)
	}

	if len(tasks) == 0 {
		return nil
	}

	reloadTask := task{typ: taskTypeReloadUnitFiles, reason: taskReasonAlwaysReloadUnitFiles}
	tasks = append(tasks, reloadTask)

	sort.Sort(sortableTasks(tasks))

	// reload unnecessary if no UnloadUnit/LoadUnit tasks
	if tasks[0].typ == taskTypeReloadUnitFiles {
		tasks = tasks[1:]
	}

	return tasks
}
Beispiel #6
0
func TestFakeUnitManagerGetUnitStates(t *testing.T) {
	fum := NewFakeUnitManager()

	err := fum.Load("hello.service", Unit{})
	if err != nil {
		t.Fatalf("Expected no error from Load(), got %v", err)
	}

	states, err := fum.GetUnitStates(pkg.NewUnsafeSet("hello.service", "goodbye.service"))
	if err != nil {
		t.Fatalf("Failed calling GetUnitStates: %v", err)
	}

	expectStates := map[string]*UnitState{
		"hello.service": &UnitState{
			LoadState:   "loaded",
			ActiveState: "active",
			SubState:    "running",
		},
	}

	if !reflect.DeepEqual(expectStates, states) {
		t.Fatalf("Received unexpected collection of UnitStates: %#v\nExpected: %#v", states, expectStates)
	}
}
Beispiel #7
0
// calculateTaskChainsForJobs compares the desired and current state of an Agent.
// The generated taskChains represent what should be done to make the desired
// state match the current state.
func (ar *AgentReconciler) calculateTaskChainsForJobs(dState, cState *AgentState) <-chan taskChain {
	tcChan := make(chan taskChain)
	go func() {
		jobs := pkg.NewUnsafeSet()
		for cName := range cState.Jobs {
			jobs.Add(cName)
		}

		for dName := range dState.Jobs {
			jobs.Add(dName)
		}

		for _, name := range jobs.Values() {
			tc := ar.calculateTaskChainForJob(dState, cState, name)
			if tc == nil {
				continue
			}
			tcChan <- *tc
		}

		close(tcChan)
	}()

	return tcChan
}
Beispiel #8
0
// RequiredTargetMetadata return all machine-related metadata from a Job's
// requirements. Valid metadata fields are strings of the form `key=value`,
// where both key and value are not the empty string.
func (j *Job) RequiredTargetMetadata() map[string]pkg.Set {
	metadata := make(map[string]pkg.Set)

	for _, key := range []string{
		deprecatedXConditionPrefix + fleetMachineMetadata,
		fleetMachineMetadata,
	} {
		for _, valuePair := range j.requirements()[key] {
			s := strings.Split(valuePair, "=")

			if len(s) != 2 {
				continue
			}

			if len(s[0]) == 0 || len(s[1]) == 0 {
				continue
			}

			if _, ok := metadata[s[0]]; !ok {
				metadata[s[0]] = pkg.NewUnsafeSet()
			}
			metadata[s[0]].Add(s[1])
		}
	}

	return metadata
}
Beispiel #9
0
func newClusterState(jobs []job.Job, unresolved []job.JobOffer, machines []machine.MachineState) *clusterState {
	oSet := pkg.NewUnsafeSet()
	for _, offer := range unresolved {
		oSet.Add(offer.Job.Name)
	}

	mSet := pkg.NewUnsafeSet()
	for _, m := range machines {
		mSet.Add(m.ID)
	}

	return &clusterState{
		jobs:     jobs,
		offers:   oSet,
		machines: mSet,
	}
}
Beispiel #10
0
func (f *FakeRegistry) SubmitJobBid(jName, machID string) {
	f.Lock()
	defer f.Unlock()

	_, ok := f.bids[jName]
	if !ok {
		f.bids[jName] = pkg.NewUnsafeSet()
	}
	f.bids[jName].Add(machID)
}
Beispiel #11
0
// ValidateOptions ensures that a set of UnitOptions is valid; if not, an error
// is returned detailing the issue encountered.  If there are several problems
// with a set of options, only the first is returned.
func ValidateOptions(opts []*schema.UnitOption) error {
	uf := schema.MapSchemaUnitOptionsToUnitFile(opts)
	j := &job.Job{
		Unit: *uf,
	}
	conflicts := pkg.NewUnsafeSet(j.Conflicts()...)
	peers := pkg.NewUnsafeSet(j.Peers()...)
	for _, peer := range peers.Values() {
		for _, conflict := range conflicts.Values() {
			matched, _ := path.Match(conflict, peer)
			if matched {
				return fmt.Errorf("unresolvable requirements: peer %q matches conflict %q", peer, conflict)
			}
		}
	}
	hasPeers := peers.Length() != 0
	hasConflicts := conflicts.Length() != 0
	_, hasReqTarget := j.RequiredTarget()
	u := &job.Unit{
		Unit: *uf,
	}
	isGlobal := u.IsGlobal()

	switch {
	case hasReqTarget && hasPeers:
		return errors.New("MachineID cannot be used with Peers")
	case hasReqTarget && hasConflicts:
		return errors.New("MachineID cannot be used with Conflicts")
	case hasReqTarget && isGlobal:
		return errors.New("MachineID cannot be used with Global")
	case isGlobal && hasPeers:
		return errors.New("Global cannot be used with Peers")
	case isGlobal && hasConflicts:
		return errors.New("Global cannot be used with Conflicts")
	}

	return nil
}
Beispiel #12
0
// calculateTasksForUnits compares the desired and current state of an Agent.
// The generated tasks represent what, in order, should be done to make the
//  desired state match the current state.
func (ar *AgentReconciler) calculateTasksForUnits(dState *AgentState, cState unitStates) []task {
	jobs := pkg.NewUnsafeSet()
	for cName := range cState {
		jobs.Add(cName)
	}

	for dName := range dState.Units {
		jobs.Add(dName)
	}

	var tasks []task
	for _, name := range jobs.Values() {
		tasks = append(tasks, ar.calculateTasksForUnit(dState, cState, name)...)
	}

	sort.Sort(sortableTasks(tasks))
	return tasks
}
Beispiel #13
0
func TestTaskManagerUnitSerialization(t *testing.T) {
	result := make(chan error)
	testMapper := func(task, *job.Unit, *Agent) (exec func() error, err error) {
		exec = func() error {
			return <-result
		}
		return
	}

	tm := taskManager{
		processing: pkg.NewUnsafeSet(),
		mapper:     testMapper,
	}

	reschan, err := tm.Do(taskChain{unit: &job.Unit{Name: "foo"}, tasks: []task{task{typ: "test"}}}, nil)
	if err != nil {
		t.Fatalf("unable to start first task: %v", err)
	}

	// the first task should block the second, as it is still in progress
	_, err = tm.Do(taskChain{unit: &job.Unit{Name: "foo"}, tasks: []task{task{typ: "test"}}}, nil)
	if err == nil {
		t.Fatalf("expected error from attempt to start second task, got nil")
	}

	result <- nil

	select {
	case res := <-reschan:
		if res.err != nil {
			t.Errorf("received unexpected error from first task: %v", err)
		}
	default:
		t.Errorf("expected reschan to be ready to receive")
	}

	// since the first task completed, this third task can start
	_, err = tm.Do(taskChain{unit: &job.Unit{Name: "foo"}, tasks: []task{task{typ: "test"}}}, nil)
	if err != nil {
		t.Fatalf("unable to start third task: %v", err)
	}

	close(result)
}
Beispiel #14
0
func TestTaskManagerTwoInFlight(t *testing.T) {
	result := make(chan error)
	testMapper := func(task, *job.Unit, *Agent) (exec func() error, err error) {
		exec = func() error {
			return <-result
		}
		return
	}

	tm := taskManager{
		processing: pkg.NewUnsafeSet(),
		mapper:     testMapper,
	}

	errchan1, err := tm.Do(taskChain{unit: &job.Unit{Name: "foo"}, tasks: []task{task{typ: "test"}}}, nil)
	if err != nil {
		t.Fatalf("unable to start task: %v", err)
	}

	errchan2, err := tm.Do(taskChain{unit: &job.Unit{Name: "bar"}, tasks: []task{task{typ: "test"}}}, nil)
	if err != nil {
		t.Fatalf("unable to start task: %v", err)
	}

	close(result)

	go func() {
		<-time.After(time.Second)
		t.Fatalf("expected errchans to be ready to receive within 1s")
	}()

	res := <-errchan1
	if res.err != nil {
		t.Fatalf("received unexpected error from task one: %v", res.err)
	}

	res = <-errchan2
	if res.err != nil {
		t.Fatalf("received unexpected error from task two: %v", res.err)
	}
}
Beispiel #15
0
// calculateTasksForJobs compares the desired and current state of an Agent.
// The generateed tasks represent what should be done to make the desired
// state match the current state.
func (ar *AgentReconciler) calculateTasksForJobs(ms *machine.MachineState, dState, cState *agentState) <-chan *task {
	taskchan := make(chan *task)
	go func() {
		jobs := pkg.NewUnsafeSet()
		for cName := range cState.jobs {
			jobs.Add(cName)
		}

		for dName := range dState.jobs {
			jobs.Add(dName)
		}

		for _, name := range jobs.Values() {
			ar.calculateTasksForJob(ms, dState, cState, name, taskchan)
		}

		close(taskchan)
	}()

	return taskchan
}
Beispiel #16
0
// Bids returns a list of machine IDs that have bid for the referenced Job
func (r *EtcdRegistry) Bids(jName string) (bids pkg.Set, err error) {
	bids = pkg.NewUnsafeSet()

	req := etcd.Get{
		Key:       path.Join(r.keyPrefix, offerPrefix, jName, "bids"),
		Recursive: true,
	}

	var resp *etcd.Result
	resp, err = r.etcd.Do(&req)
	if err != nil {
		if isKeyNotFound(err) {
			err = nil
		}
		return
	}

	for _, node := range resp.Node.Nodes {
		machID := path.Base(node.Key)
		bids.Add(machID)
	}

	return
}
Beispiel #17
0
func TestCalculateTasksForOffer(t *testing.T) {
	tests := []struct {
		mState *machine.MachineState
		dState *agentState
		job    *job.Job
		bids   pkg.Set

		tasks []task
	}{
		// no bid submitted yet and able to run
		{
			mState: &machine.MachineState{ID: "XXX"},
			dState: newAgentState(),
			job: &job.Job{
				Name:        "foo.service",
				TargetState: jsLaunched,
				Unit:        fleetUnit(t),
			},
			bids: pkg.NewUnsafeSet(),
			tasks: []task{
				task{
					Type: taskTypeSubmitBid,
					Job: &job.Job{
						Name:        "foo.service",
						TargetState: jsLaunched,
						Unit:        fleetUnit(t),
					},
					Reason: taskReasonAbleToResolveOffer,
				},
			},
		},

		// no bid submitted but unable to run
		{
			mState: &machine.MachineState{ID: "XXX"},
			dState: newAgentState(),
			job: &job.Job{
				Name:        "foo.service",
				TargetState: jsLaunched,
				Unit:        fleetUnit(t, "X-ConditionMachineID=YYY"),
			},
			bids:  pkg.NewUnsafeSet(),
			tasks: []task{},
		},

		// bid already submitted
		{
			mState: &machine.MachineState{ID: "XXX"},
			dState: newAgentState(),
			job: &job.Job{
				TargetState: jsLaunched,
				Unit:        fleetUnit(t),
			},
			bids:  pkg.NewUnsafeSet("XXX"),
			tasks: []task{},
		},
	}

	for i, tt := range tests {
		ar := NewReconciler(registry.NewFakeRegistry(), nil)
		taskchan := make(chan *task)
		tasks := []task{}
		go func() {
			ar.calculateTasksForOffer(tt.dState, tt.mState, tt.job, tt.bids, taskchan)
			close(taskchan)
		}()

		for t := range taskchan {
			tasks = append(tasks, *t)
		}

		if !reflect.DeepEqual(tt.tasks, tasks) {
			t.Errorf("case %d: calculated incorrect list of tasks\nexpected=%v\nreceived=%v\n", i, tt.tasks, tasks)
		}
	}
}
Beispiel #18
0
	// Machine metadata key in the unit file
	fleetMachineMetadata = "MachineMetadata"
	// Require that the unit be scheduled on every machine in the cluster
	fleetGlobal = "Global"

	deprecatedXPrefix          = "X-"
	deprecatedXConditionPrefix = "X-Condition"
)

// validRequirements encapsulates all current and deprecated unit file requirement keys
var validRequirements = pkg.NewUnsafeSet(
	fleetMachineID,
	deprecatedXConditionPrefix+fleetMachineID,
	deprecatedXConditionPrefix+fleetMachineBootID,
	deprecatedXConditionPrefix+fleetMachineOf,
	fleetMachineOf,
	deprecatedXPrefix+fleetConflicts,
	fleetConflicts,
	deprecatedXConditionPrefix+fleetMachineMetadata,
	fleetMachineMetadata,
	fleetGlobal,
)

func ParseJobState(s string) (JobState, error) {
	js := JobState(s)

	var err error
	if js != JobStateInactive && js != JobStateLoaded && js != JobStateLaunched {
		err = fmt.Errorf("invalid value %q for JobState", s)
		js = JobStateInactive
	}
Beispiel #19
0
func newTaskManager() *taskManager {
	return &taskManager{
		processing: pkg.NewUnsafeSet(),
		mapper:     mapTaskToFunc,
	}
}
Beispiel #20
0
	unitNameMax    = 256
	digits         = "0123456789"
	lowercase      = "abcdefghijklmnopqrstuvwxyz"
	uppercase      = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	alphanumerical = digits + lowercase + uppercase
	validChars     = alphanumerical + `:-_.\@`
)

var validUnitTypes = pkg.NewUnsafeSet(
	"service",
	"socket",
	"busname",
	"target",
	"snapshot",
	"device",
	"mount",
	"automount",
	"swap",
	"timer",
	"path",
	"slice",
	"scope",
)

// ValidateName ensures that a given unit name is valid; if not, an error is
// returned describing the first issue encountered.
// systemd reference: `unit_name_is_valid` in `unit-name.c`
func ValidateName(name string) error {
	length := len(name)
	if length == 0 {
		return errors.New("unit name cannot be empty")
Beispiel #21
0
func TestJobRequiredMetadata(t *testing.T) {
	testCases := []struct {
		unit string
		out  map[string]pkg.Set
	}{
		// no metadata
		{
			`[X-Fleet]`,
			map[string]pkg.Set{},
		},
		// simplest case - one key/value
		{
			`[X-Fleet]
MachineMetadata=foo=bar`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar"),
			},
		},
		// multiple different values for a key in one line
		{
			`[X-Fleet]
MachineMetadata="foo=bar" "foo=baz"`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar", "baz"),
			},
		},
		// multiple different values for a key in different lines
		{
			`[X-Fleet]
MachineMetadata=foo=bar
MachineMetadata=foo=baz
MachineMetadata=foo=asdf`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar", "baz", "asdf"),
			},
		},
		// multiple different key-value pairs in a single line
		{
			`[X-Fleet]
MachineMetadata="foo=bar" "duck=quack"`,
			map[string]pkg.Set{
				"foo":  pkg.NewUnsafeSet("bar"),
				"duck": pkg.NewUnsafeSet("quack"),
			},
		},
		// multiple different key-value pairs in different lines
		{
			`[X-Fleet]
MachineMetadata=foo=bar
MachineMetadata=dog=woof
MachineMetadata=cat=miaow`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar"),
				"dog": pkg.NewUnsafeSet("woof"),
				"cat": pkg.NewUnsafeSet("miaow"),
			},
		},
		// support deprecated prefixed syntax
		{
			`[X-Fleet]
X-ConditionMachineMetadata=foo=bar`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar"),
			},
		},
		// support deprecated prefixed syntax mixed with modern syntax
		{
			`[X-Fleet]
MachineMetadata=foo=bar
X-ConditionMachineMetadata=foo=asdf`,
			map[string]pkg.Set{
				"foo": pkg.NewUnsafeSet("bar", "asdf"),
			},
		},
		// bad fields just get ignored
		{
			`[X-Fleet]
MachineMetadata=foo=`,
			map[string]pkg.Set{},
		},
		{
			`[X-Fleet]
MachineMetadata==asdf`,
			map[string]pkg.Set{},
		},
		{
			`[X-Fleet]
MachineMetadata=foo=asdf=WHAT`,
			map[string]pkg.Set{},
		},
		// mix everything up
		{
			`[X-Fleet]
MachineMetadata=ignored=
MachineMetadata=oh=yeah
MachineMetadata=whynot=zoidberg
X-ConditionMachineMetadata=oh=no
X-ConditionMachineMetadata="one=abc" "two=def"`,
			map[string]pkg.Set{
				"oh":     pkg.NewUnsafeSet("yeah", "no"),
				"whynot": pkg.NewUnsafeSet("zoidberg"),
				"one":    pkg.NewUnsafeSet("abc"),
				"two":    pkg.NewUnsafeSet("def"),
			},
		},
	}
	for i, tt := range testCases {
		j := NewJob("echo.service", *newUnit(t, tt.unit))
		md := j.RequiredTargetMetadata()
		if !reflect.DeepEqual(md, tt.out) {
			t.Errorf("case %d: metadata differs", i)
			t.Logf("got: %#v", md)
			t.Logf("want: %#v", tt.out)
		}
	}
}