<commit_before>package agent

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/buildkite/agent/api"
	"github.com/buildkite/agent/logger"
	"github.com/buildkite/agent/metrics"
	"github.com/buildkite/agent/proctitle"
	"github.com/buildkite/agent/retry"
)

type AgentWorkerConfig struct {
	// Whether to set debug in the job
	Debug bool

	// The endpoint that should be used when communicating with the API
	Endpoint string

	// Whether to disable http for the API
	DisableHTTP2 bool

	// The configuration of the agent from the CLI
	AgentConfiguration AgentConfiguration
}

type AgentWorker struct {
	// Tracks the last successful heartbeat and ping
	// NOTE: to avoid alignment issues on ARM architectures when
	// using atomic.StoreInt64 we need to keep this at the beginning
	// of the struct
	lastPing, lastHeartbeat int64

	// The API Client used when this agent is communicating with the API
	apiClient *api.Client

	// The logger instance to use
	logger logger.Logger

	// The configuration of the agent from the CLI
	agentConfiguration AgentConfiguration

	// The registered agent API record
	agent *api.AgentRegisterResponse

	// Metric collection for the agent
	metricsCollector *metrics.Collector

	// Metrics scope for the agent
	metrics *metrics.Scope

	// Whether to enable debug
	debug bool

	// Track the idle disconnect timer and a cross-agent monitor
	idleTimer   *time.Timer
	idleMonitor *IdleMonitor

	// Stop controls
	stop      chan struct{}
	stopping  bool
	stopMutex sync.Mutex

	// When this worker runs a job, we'll store an instance of the
	// JobRunner here
	jobRunner *JobRunner
}

// Creates the agent worker and initializes it's API Client
func NewAgentWorker(l logger.Logger, a *api.AgentRegisterResponse, m *metrics.Collector, c AgentWorkerConfig) *AgentWorker {
	var endpoint string
	if a.Endpoint != "" {
		endpoint = a.Endpoint
	} else {
		endpoint = c.Endpoint
	}

	// Create an APIClient with the agent's access token
	apiClient := NewAPIClient(l, APIClientConfig{
		Endpoint:     endpoint,
		Token:        a.AccessToken,
		DisableHTTP2: c.DisableHTTP2,
	})

	return &AgentWorker{
		logger:             l,
		agent:              a,
		metricsCollector:   m,
		apiClient:          apiClient,
		debug:              c.Debug,
		agentConfiguration: c.AgentConfiguration,
		stop:               make(chan struct{}),
	}
}

// Starts the agent worker
func (a *AgentWorker) Start(idle *IdleMonitor) error {
	a.metrics = a.metricsCollector.Scope(metrics.Tags{
		"agent_name": a.agent.Name,
	})

	// Start running our metrics collector
	if err := a.metricsCollector.Start(); err != nil {
		return err
	}
	defer a.metricsCollector.Stop()

	// Create the intervals we'll be using
	pingInterval := time.Second * time.Duration(a.agent.PingInterval)
	heartbeatInterval := time.Second * time.Duration(a.agent.HeartbeatInterval)

	// Create the ticker
	pingTicker := time.NewTicker(pingInterval)
	defer pingTicker.Stop()

	// Use a context to run heartbeats for as long as the agent runs for
	heartbeatCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Setup and start the heartbeater
	go func() {
		for {
			select {
			case <-time.After(heartbeatInterval):
				err := a.Heartbeat()
				if err != nil {
					// Get the last heartbeat time to the nearest microsecond
					lastHeartbeat := time.Unix(atomic.LoadInt64(&a.lastPing), 0)

					a.logger.Error("Failed to heartbeat %s. Will try again in %s. (Last successful was %v ago)",
						err, heartbeatInterval, time.Now().Sub(lastHeartbeat))
				}

			case <-heartbeatCtx.Done():
				a.logger.Debug("Stopping heartbeats")
				return
			}
		}
	}()

	a.idleMonitor = idle

	// Setup an idle timer to disconnect after periods of idleness
	if a.agentConfiguration.DisconnectAfterIdleTimeout > 0 {
		a.idleTimer = time.NewTimer(time.Second * time.Duration(a.agentConfiguration.DisconnectAfterIdleTimeout))
		go func() {
			for {
				select {
				case <-a.idleTimer.C:
					// Mark this agent as idle in the shared idle monitor
					a.idleMonitor.MarkIdle(a.agent.UUID)

					// Only terminate if all agents in the pool are idle, otherwise extend the timer
					if a.idleMonitor.Idle() {
						a.logger.Info("Agent has been idle for %d seconds",
							a.agentConfiguration.DisconnectAfterIdleTimeout)
						a.stopIfIdle()
					} else {
						// Extend the timer by the smaller of 10% of the idle timer or 60 seconds
						extendDuration := (time.Second * time.Duration(a.agentConfiguration.DisconnectAfterIdleTimeout)) / 10
						if extendDuration > (time.Second * 60) {
							extendDuration = time.Second * 60
						}

						a.logger.Debug("Agent has been idle for %d seconds, but other agents are active so extending for %v",
							a.agentConfiguration.DisconnectAfterIdleTimeout, extendDuration)
						a.idleTimer.Reset(extendDuration)
					}

				case <-a.stop:
					a.logger.Debug("Stopping the idle ticker")
					return
				}
			}
		}()
	}

	a.logger.Info("Waiting for work...")

	// Continue this loop until the closing of the stop channel signals termination
	for {
		if !a.stopping {
			a.Ping()
		}

		select {
		case <-pingTicker.C:
			continue
		case <-a.stop:
			return nil
		}
	}
}

// Stops the agent from accepting new work and cancels any current work it's
// running
func (a *AgentWorker) Stop(graceful bool) {
	// Only allow one stop to run at a time (because we're playing with
	// channels)
	a.stopMutex.Lock()
	defer a.stopMutex.Unlock()

	if graceful {
		if a.stopping {
			a.logger.Warn("Agent is already gracefully stopping...")
		} else {
			// If we have a job, tell the user that we'll wait for
			// it to finish before disconnecting
			if a.jobRunner != nil {
				a.logger.Info("Gracefully stopping agent. Waiting for current job to finish before disconnecting...")
			} else {
				a.logger.Info("Gracefully stopping agent. Since there is no job running, the agent will disconnect immediately")
			}
		}
	} else {
		// If there's a job running, kill it, then disconnect
		if a.jobRunner != nil {
			a.logger.Info("Forcefully stopping agent. The current job will be canceled before disconnecting...")

			// Kill the current job. Doesn't do anything if the job
			// is already being killed, so it's safe to call
			// multiple times.
			a.jobRunner.Cancel()
		} else {
			a.logger.Info("Forcefully stopping agent. Since there is no job running, the agent will disconnect immediately")
		}
	}

	// We don't need to do the below operations again since we've already
	// done them before
	if a.stopping {
		return
	}

	// Update the proc title
	a.UpdateProcTitle("stopping")

	// Use the closure of the stop channel as a signal to the main run loop in Start()
	// to stop looping and terminate
	close(a.stop)

	// Mark the agent as stopping
	a.stopping = true
}

func (a *AgentWorker) stopIfIdle() {
	if a.jobRunner == nil && !a.stopping {
		a.Stop(true)
	} else {
		a.logger.Debug("Agent is running a job, going to let it finish it's work")
	}
}

// Connects the agent to the Buildkite Agent API, retrying up to 30 times if it
// fails.
func (a *AgentWorker) Connect() error {
	a.logger.Info("Connecting to Buildkite...")

	// Update the proc title
	a.UpdateProcTitle("connecting")

	return retry.Do(func(s *retry.Stats) error {
		_, err := a.apiClient.Agents.Connect()
		if err != nil {
			a.logger.Warn("%s (%s)", err, s)
		}

		return err
	}, &retry.Config{Maximum: 10, Interval: 5 * time.Second})
}

// Performs a heatbeat
func (a *AgentWorker) Heartbeat() error {
	var beat *api.Heartbeat
	var err error

	// Retry the heartbeat a few times
	err = retry.Do(func(s *retry.Stats) error {
		beat, _, err = a.apiClient.Heartbeats.Beat()
		if err != nil {
			a.logger.Warn("%s (%s)", err, s)
		}
		return err
	}, &retry.Config{Maximum: 5, Interval: 5 * time.Second})

	if err != nil {
		return err
	}

	// Track a timestamp for the successful heartbeat for better errors
	atomic.StoreInt64(&a.lastHeartbeat, time.Now().Unix())

	a.logger.Debug("Heartbeat sent at %s and received at %s", beat.SentAt, beat.ReceivedAt)
	return nil
}

// Performs a ping, which returns what action the agent should take next.
func (a *AgentWorker) Ping() {
	// Update the proc title
	a.UpdateProcTitle("pinging")

	ping, _, err := a.apiClient.Pings.Get()
	if err != nil {
		// Get the last ping time to the nearest microsecond
		lastPing := time.Unix(atomic.LoadInt64(&a.lastPing), 0)

		// If a ping fails, we don't really care, because it'll
		// ping again after the interval.
		a.logger.Warn("Failed to ping: %s (Last successful was %v ago)", err, time.Now().Sub(lastPing))

		return
	}

	// Track a timestamp for the successful ping for better errors
	atomic.StoreInt64(&a.lastPing, time.Now().Unix())

	// Should we switch endpoints?
	if ping.Endpoint != "" && ping.Endpoint != a.agent.Endpoint {
		// Before switching to the new one, do a ping test to make sure it's
		// valid. If it is, switch and carry on, otherwise ignore the switch
		// for now.
		newAPIClient := NewAPIClient(a.logger, APIClientConfig{
			Endpoint: ping.Endpoint,
			Token:    a.agent.AccessToken,
		})

		newPing, _, err := newAPIClient.Pings.Get()
		if err != nil {
			a.logger.Warn("Failed to ping the new endpoint %s - ignoring switch for now (%s)", ping.Endpoint, err)
		} else {
			// Replace the APIClient and process the new ping
			a.apiClient = newAPIClient
			a.agent.Endpoint = ping.Endpoint
			ping = newPing
		}
	}

	// Is there a message that should be shown in the logs?
	if ping.Message != "" {
		a.logger.Info(ping.Message)
	}

	// Should the agent disconnect?
	if ping.Action == "disconnect" {
		a.Stop(false)
		return
	}

	// If we don't have a job, there's nothing to do!
	if ping.Job == nil {
		// Update the proc title
		a.UpdateProcTitle("idle")

		return
	}

	// Update the proc title
	a.UpdateProcTitle(fmt.Sprintf("job %s", strings.Split(ping.Job.ID, "-")[0]))

	a.logger.Info("Assigned job %s. Accepting...", ping.Job.ID)

	// Accept the job. We'll retry on connection related issues, but if
	// Buildkite returns a 422 or 500 for example, we'll just bail out,
	// re-ping, and try the whole process again.
	var accepted *api.Job
	retry.Do(func(s *retry.Stats) error {
		accepted, _, err = a.apiClient.Jobs.Accept(ping.Job)

		if err != nil {
			if api.IsRetryableError(err) {
				a.logger.Warn("%s (%s)", err, s)
			} else {
				a.logger.Warn("Buildkite rejected the call to accept the job (%s)", err)
				s.Break()
			}
		}

		return err
	}, &retry.Config{Maximum: 30, Interval: 5 * time.Second})

	// If `accepted` is nil, then the job was never accepted
	if accepted == nil {
		a.logger.Error("Failed to accept job")
		return
	}

	jobMetricsScope := a.metrics.With(metrics.Tags{
		`pipeline`: accepted.Env[`BUILDKITE_PIPELINE_SLUG`],
		`org`:      accepted.Env[`BUILDKITE_ORGANIZATION_SLUG`],
		`branch`:   accepted.Env[`BUILDKITE_BRANCH`],
		`source`:   accepted.Env[`BUILDKITE_SOURCE`],
	})

	// Now that the job has been accepted, we can start it.
	a.jobRunner, err = NewJobRunner(a.logger, jobMetricsScope, a.agent, accepted, JobRunnerConfig{
		Debug:              a.debug,
		Endpoint:           accepted.Endpoint,
		AgentConfiguration: a.agentConfiguration,
	})

	// Was there an error creating the job runner?
	if err != nil {
		a.logger.Error("Failed to initialize job: %s", err)
		return
	}

	// Start running the job
	if err = a.jobRunner.Run(); err != nil {
		a.logger.Error("Failed to run job: %s", err)
	}

	// No more job, no more runner.
	a.jobRunner = nil

	if a.agentConfiguration.DisconnectAfterJob {
		a.logger.Info("Job finished. Disconnecting...")

		// Tell the agent to finish up
		a.Stop(true)
	}

	if a.agentConfiguration.DisconnectAfterIdleTimeout > 0 {
		a.logger.Info("Job finished. Resetting idle timer...")
		a.idleTimer.Reset(time.Second * time.Duration(a.agentConfiguration.DisconnectAfterIdleTimeout))
		a.idleMonitor.MarkBusy(a.agent.UUID)
	}
}

// Disconnects the agent from the Buildkite Agent API, doesn't bother retrying
// because we want to disconnect as fast as possible.
func (a *AgentWorker) Disconnect() error {
	a.logger.Info("Disconnecting...")

	// Update the proc title
	a.UpdateProcTitle("disconnecting")

	_, err := a.apiClient.Agents.Disconnect()
	if err != nil {
		a.logger.Warn("There was an error sending the disconnect API call to Buildkite. If this agent still appears online, you may have to manually stop it (%s)", err)
	}

	return err
}

func (a *AgentWorker) UpdateProcTitle(action string) {
	proctitle.Replace(fmt.Sprintf("buildkite-agent v%s [%s]", Version(), action))
}

type IdleMonitor struct {
	sync.Mutex
	totalAgents int
	idle        map[string]struct{}
}

func NewIdleMonitor(totalAgents int) *IdleMonitor {
	return &IdleMonitor{
		totalAgents: totalAgents,
		idle:        map[string]struct{}{},
	}
}

func (i *IdleMonitor) Idle() bool {
	i.Lock()
	defer i.Unlock()
	return len(i.idle) == i.totalAgents
}

func (i *IdleMonitor) MarkIdle(agentUUID string) {
	i.Lock()
	defer i.Unlock()
	i.idle[agentUUID] = struct{}{}
}

func (i *IdleMonitor) MarkBusy(agentUUID string) {
	i.Lock()
	defer i.Unlock()
	delete(i.idle, agentUUID)
}
<commit_msg>Simplify how we handle idle timeouts<commit_after>package agent

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/buildkite/agent/api"
	"github.com/buildkite/agent/logger"
	"github.com/buildkite/agent/metrics"
	"github.com/buildkite/agent/proctitle"
	"github.com/buildkite/agent/retry"
)

type AgentWorkerConfig struct {
	// Whether to set debug in the job
	Debug bool

	// The endpoint that should be used when communicating with the API
	Endpoint string

	// Whether to disable http for the API
	DisableHTTP2 bool

	// The configuration of the agent from the CLI
	AgentConfiguration AgentConfiguration
}

type AgentWorker struct {
	// Tracks the last successful heartbeat and ping
	// NOTE: to avoid alignment issues on ARM architectures when
	// using atomic.StoreInt64 we need to keep this at the beginning
	// of the struct
	lastPing, lastHeartbeat int64

	// The API Client used when this agent is communicating with the API
	apiClient *api.Client

	// The logger instance to use
	logger logger.Logger

	// The configuration of the agent from the CLI
	agentConfiguration AgentConfiguration

	// The registered agent API record
	agent *api.AgentRegisterResponse

	// Metric collection for the agent
	metricsCollector *metrics.Collector

	// Metrics scope for the agent
	metrics *metrics.Scope

	// Whether to enable debug
	debug bool

	// Stop controls
	stop      chan struct{}
	stopping  bool
	stopMutex sync.Mutex

	// When this worker runs a job, we'll store an instance of the
	// JobRunner here
	jobRunner *JobRunner
}

// Creates the agent worker and initializes it's API Client
func NewAgentWorker(l logger.Logger, a *api.AgentRegisterResponse, m *metrics.Collector, c AgentWorkerConfig) *AgentWorker {
	var endpoint string
	if a.Endpoint != "" {
		endpoint = a.Endpoint
	} else {
		endpoint = c.Endpoint
	}

	// Create an APIClient with the agent's access token
	apiClient := NewAPIClient(l, APIClientConfig{
		Endpoint:     endpoint,
		Token:        a.AccessToken,
		DisableHTTP2: c.DisableHTTP2,
	})

	return &AgentWorker{
		logger:             l,
		agent:              a,
		metricsCollector:   m,
		apiClient:          apiClient,
		debug:              c.Debug,
		agentConfiguration: c.AgentConfiguration,
		stop:               make(chan struct{}),
	}
}

// Starts the agent worker
func (a *AgentWorker) Start(idleMonitor *IdleMonitor) error {
	a.metrics = a.metricsCollector.Scope(metrics.Tags{
		"agent_name": a.agent.Name,
	})

	// Start running our metrics collector
	if err := a.metricsCollector.Start(); err != nil {
		return err
	}
	defer a.metricsCollector.Stop()

	// Create the intervals we'll be using
	pingInterval := time.Second * time.Duration(a.agent.PingInterval)
	heartbeatInterval := time.Second * time.Duration(a.agent.HeartbeatInterval)

	// Create the ticker
	pingTicker := time.NewTicker(pingInterval)
	defer pingTicker.Stop()

	// Use a context to run heartbeats for as long as the agent runs for
	heartbeatCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Setup and start the heartbeater
	go func() {
		for {
			select {
			case <-time.After(heartbeatInterval):
				err := a.Heartbeat()
				if err != nil {
					// Get the last heartbeat time to the nearest microsecond
					lastHeartbeat := time.Unix(atomic.LoadInt64(&a.lastPing), 0)

					a.logger.Error("Failed to heartbeat %s. Will try again in %s. (Last successful was %v ago)",
						err, heartbeatInterval, time.Now().Sub(lastHeartbeat))
				}

			case <-heartbeatCtx.Done():
				a.logger.Debug("Stopping heartbeats")
				return
			}
		}
	}()

	lastActionTime := time.Now()
	a.logger.Info("Waiting for work...")

	// Continue this loop until the closing of the stop channel signals termination
	for {
		if !a.stopping {
			job, err := a.Ping()
			if err != nil {
				a.logger.Warn("%v", err)
			} else if job != nil {
				// Let other agents know this agent is now busy and
				// not to idle terminate
				idleMonitor.MarkBusy(a.agent.UUID)

				// Runs the job, only errors if something goes wrong
				if runErr := a.AcceptAndRun(job); runErr != nil {
					a.logger.Error("%v", runErr)
				} else {
					if a.agentConfiguration.DisconnectAfterJob {
						a.logger.Info("Job finished. Disconnecting...")
						return nil
					}
					lastActionTime = time.Now()
				}
			}

			// Handle disconnect after idle timeout (and deprecated disconnect-after-job-timeout)
			if a.agentConfiguration.DisconnectAfterIdleTimeout > 0 {
				idleDeadline := lastActionTime.Add(time.Second *
					time.Duration(a.agentConfiguration.DisconnectAfterIdleTimeout))

				if time.Now().After(idleDeadline) {
					// Let other agents know this agent is now idle and termination
					// is possible
					idleMonitor.MarkIdle(a.agent.UUID)

					// But only terminate if everyone else is also idle
					if idleMonitor.Idle() {
						a.logger.Info("All agents have been idle for %d seconds. Disconnecting...",
							a.agentConfiguration.DisconnectAfterIdleTimeout)
						return nil
					} else {
						a.logger.Debug("Agent has been idle for %.f seconds, but other agents haven't",
							time.Now().Sub(lastActionTime).Seconds())
					}
				}
			}
		}

		select {
		case <-pingTicker.C:
			continue
		case <-a.stop:
			return nil
		}
	}
}

// Stops the agent from accepting new work and cancels any current work it's
// running
func (a *AgentWorker) Stop(graceful bool) {
	// Only allow one stop to run at a time (because we're playing with
	// channels)
	a.stopMutex.Lock()
	defer a.stopMutex.Unlock()

	if graceful {
		if a.stopping {
			a.logger.Warn("Agent is already gracefully stopping...")
		} else {
			// If we have a job, tell the user that we'll wait for
			// it to finish before disconnecting
			if a.jobRunner != nil {
				a.logger.Info("Gracefully stopping agent. Waiting for current job to finish before disconnecting...")
			} else {
				a.logger.Info("Gracefully stopping agent. Since there is no job running, the agent will disconnect immediately")
			}
		}
	} else {
		// If there's a job running, kill it, then disconnect
		if a.jobRunner != nil {
			a.logger.Info("Forcefully stopping agent. The current job will be canceled before disconnecting...")

			// Kill the current job. Doesn't do anything if the job
			// is already being killed, so it's safe to call
			// multiple times.
			a.jobRunner.Cancel()
		} else {
			a.logger.Info("Forcefully stopping agent. Since there is no job running, the agent will disconnect immediately")
		}
	}

	// We don't need to do the below operations again since we've already
	// done them before
	if a.stopping {
		return
	}

	// Update the proc title
	a.UpdateProcTitle("stopping")

	// Use the closure of the stop channel as a signal to the main run loop in Start()
	// to stop looping and terminate
	close(a.stop)

	// Mark the agent as stopping
	a.stopping = true
}

// Connects the agent to the Buildkite Agent API, retrying up to 30 times if it
// fails.
func (a *AgentWorker) Connect() error {
	a.logger.Info("Connecting to Buildkite...")

	// Update the proc title
	a.UpdateProcTitle("connecting")

	return retry.Do(func(s *retry.Stats) error {
		_, err := a.apiClient.Agents.Connect()
		if err != nil {
			a.logger.Warn("%s (%s)", err, s)
		}

		return err
	}, &retry.Config{Maximum: 10, Interval: 5 * time.Second})
}

// Performs a heatbeat
func (a *AgentWorker) Heartbeat() error {
	var beat *api.Heartbeat
	var err error

	// Retry the heartbeat a few times
	err = retry.Do(func(s *retry.Stats) error {
		beat, _, err = a.apiClient.Heartbeats.Beat()
		if err != nil {
			a.logger.Warn("%s (%s)", err, s)
		}
		return err
	}, &retry.Config{Maximum: 5, Interval: 5 * time.Second})

	if err != nil {
		return err
	}

	// Track a timestamp for the successful heartbeat for better errors
	atomic.StoreInt64(&a.lastHeartbeat, time.Now().Unix())

	a.logger.Debug("Heartbeat sent at %s and received at %s", beat.SentAt, beat.ReceivedAt)
	return nil
}

// Performs a ping that checks Buildkite for a job or action to take
// Returns a job, or nil if none is found
func (a *AgentWorker) Ping() (*api.Job, error) {
	// Update the proc title
	a.UpdateProcTitle("pinging")

	ping, _, err := a.apiClient.Pings.Get()
	if err != nil {
		// Get the last ping time to the nearest microsecond
		lastPing := time.Unix(atomic.LoadInt64(&a.lastPing), 0)

		// If a ping fails, we don't really care, because it'll
		// ping again after the interval.
		return nil, fmt.Errorf("Failed to ping: %v (Last successful was %v ago)", err, time.Now().Sub(lastPing))
	}

	// Track a timestamp for the successful ping for better errors
	atomic.StoreInt64(&a.lastPing, time.Now().Unix())

	// Should we switch endpoints?
	if ping.Endpoint != "" && ping.Endpoint != a.agent.Endpoint {
		// Before switching to the new one, do a ping test to make sure it's
		// valid. If it is, switch and carry on, otherwise ignore the switch
		// for now.
		newAPIClient := NewAPIClient(a.logger, APIClientConfig{
			Endpoint: ping.Endpoint,
			Token:    a.agent.AccessToken,
		})

		newPing, _, err := newAPIClient.Pings.Get()
		if err != nil {
			a.logger.Warn("Failed to ping the new endpoint %s - ignoring switch for now (%s)", ping.Endpoint, err)
		} else {
			// Replace the APIClient and process the new ping
			a.apiClient = newAPIClient
			a.agent.Endpoint = ping.Endpoint
			ping = newPing
		}
	}

	// Is there a message that should be shown in the logs?
	if ping.Message != "" {
		a.logger.Info(ping.Message)
	}

	// Should the agent disconnect?
	if ping.Action == "disconnect" {
		a.Stop(false)
		return nil, nil
	}

	// If we don't have a job, there's nothing to do!
	if ping.Job == nil {
		// Update the proc title
		a.UpdateProcTitle("idle")
		return nil, nil
	}

	return ping.Job, nil
}

// Accepts a job and runs it, only returns an error if something goes wrong
func (a *AgentWorker) AcceptAndRun(job *api.Job) error {
	a.UpdateProcTitle(fmt.Sprintf("job %s", strings.Split(job.ID, "-")[0]))

	a.logger.Info("Assigned job %s. Accepting...", job.ID)

	// Accept the job. We'll retry on connection related issues, but if
	// Buildkite returns a 422 or 500 for example, we'll just bail out,
	// re-ping, and try the whole process again.
	var accepted *api.Job
	err := retry.Do(func(s *retry.Stats) error {
		var err error
		accepted, _, err = a.apiClient.Jobs.Accept(job)
		if err != nil {
			if api.IsRetryableError(err) {
				a.logger.Warn("%s (%s)", err, s)
			} else {
				a.logger.Warn("Buildkite rejected the call to accept the job (%s)", err)
				s.Break()
			}
		}

		return err
	}, &retry.Config{Maximum: 30, Interval: 5 * time.Second})

	// If `accepted` is nil, then the job was never accepted
	if accepted == nil {
		return fmt.Errorf("Failed to accept job: %v", err)
	}

	jobMetricsScope := a.metrics.With(metrics.Tags{
		`pipeline`: accepted.Env[`BUILDKITE_PIPELINE_SLUG`],
		`org`:      accepted.Env[`BUILDKITE_ORGANIZATION_SLUG`],
		`branch`:   accepted.Env[`BUILDKITE_BRANCH`],
		`source`:   accepted.Env[`BUILDKITE_SOURCE`],
	})

	defer func() {
		// No more job, no more runner.
		a.jobRunner = nil
	}()

	// Now that the job has been accepted, we can start it.
	a.jobRunner, err = NewJobRunner(a.logger, jobMetricsScope, a.agent, accepted, JobRunnerConfig{
		Debug:              a.debug,
		Endpoint:           accepted.Endpoint,
		AgentConfiguration: a.agentConfiguration,
	})

	// Was there an error creating the job runner?
	if err != nil {
		return fmt.Errorf("Failed to initialize job: %v", err)
	}

	// Start running the job
	if err = a.jobRunner.Run(); err != nil {
		return fmt.Errorf("Failed to run job: %v", err)
	}

	return nil
}

// Disconnects the agent from the Buildkite Agent API, doesn't bother retrying
// because we want to disconnect as fast as possible.
func (a *AgentWorker) Disconnect() error {
	a.logger.Info("Disconnecting...")

	// Update the proc title
	a.UpdateProcTitle("disconnecting")

	_, err := a.apiClient.Agents.Disconnect()
	if err != nil {
		a.logger.Warn("There was an error sending the disconnect API call to Buildkite. If this agent still appears online, you may have to manually stop it (%s)", err)
	}

	return err
}

func (a *AgentWorker) UpdateProcTitle(action string) {
	proctitle.Replace(fmt.Sprintf("buildkite-agent v%s [%s]", Version(), action))
}

type IdleMonitor struct {
	sync.Mutex
	totalAgents int
	idle        map[string]struct{}
}

func NewIdleMonitor(totalAgents int) *IdleMonitor {
	return &IdleMonitor{
		totalAgents: totalAgents,
		idle:        map[string]struct{}{},
	}
}

func (i *IdleMonitor) Idle() bool {
	i.Lock()
	defer i.Unlock()
	return len(i.idle) == i.totalAgents
}

func (i *IdleMonitor) MarkIdle(agentUUID string) {
	i.Lock()
	defer i.Unlock()
	i.idle[agentUUID] = struct{}{}
}

func (i *IdleMonitor) MarkBusy(agentUUID string) {
	i.Lock()
	defer i.Unlock()
	delete(i.idle, agentUUID)
}
<|endoftext|>


<commit_before>package i9k

import (
	context "context"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/golang/protobuf/ptypes"

	"github.com/superp00t/etc"

	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

const (
	MaxChunkSize = 1048576 * 32
)

type File struct {
	Path string
	*os.File
}

// FileStorageServer can be embedded to have forward compatible implementations.
type FileStorageServer struct {
	Base        etc.Path
	Fingerprint string
}

// check identity of peer
func (fss *FileStorageServer) verify(ctx context.Context) error {
	fp, err := GetPeerFingerprint(ctx)
	if err != nil {
		return err
	}

	if fp != fss.Fingerprint {
		return err
	}

	return nil
}

func (fss *FileStorageServer) getSafeFilePath(path string) (etc.Path, error) {
	subPath := etc.ParseUnixPath(path)
	if len(subPath) < 1 {
		return nil, status.Errorf(codes.InvalidArgument, "Invalid path")
	}

	if subPath[0] == "..." {
		return nil, status.Errorf(codes.InvalidArgument, "Must use relative paths")
	}

	safePath := fss.Base.GetSub(subPath)

	return safePath, nil
}

func (fss *FileStorageServer) statInfo(finf os.FileInfo) *StatResponse {
	tm, err := ptypes.TimestampProto(finf.ModTime())
	if err != nil {
		panic(err)
	}

	return &StatResponse{
		FileName:        finf.Name(),
		FileSize:        finf.Size(),
		FileMode:        uint32(finf.Mode()),
		FileModTime:     tm,
		FileIsDirectory: finf.IsDir(),
	}
}

func (fss *FileStorageServer) Stat(ctx context.Context, req *StatRequest) (*StatResponse, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}
	safePath, err := fss.getSafeFilePath(req.Path)
	if err != nil {
		return nil, err
	}

	finf, err := os.Stat(safePath.Render())
	if err != nil {
		return nil, err
	}

	if finf.Size() == 0 {
		panic("invalid size")
	}

	sr := fss.statInfo(finf)

	return sr, nil
}

func (fss *FileStorageServer) WriteAll(srv Storage_WriteAllServer) error {
	if err := fss.verify(srv.Context()); err != nil {
		return err
	}

	var file *os.File

	for {
		data, err := srv.Recv()
		if err == io.EOF {
			break
		}

		if err != nil {
			return err
		}

		if file == nil {
			fh := data.GetFileName()
			if fh == "" {
				return status.Errorf(codes.InvalidArgument, "handle should be first element in stream")
			}

			filePath, err := fss.getSafeFilePath(fh)
			if err != nil {
				return err
			}

			if _, err = os.Stat(filePath.Render()); err == nil {
				os.Remove(filePath.Render())
			}

			file, err = os.OpenFile(filePath.Render(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0700)
			if err != nil {
				panic(err)
			}
			continue
		}

		bytes := data.GetData()
		if bytes == nil {
			return status.Errorf(codes.InvalidArgument, "no data")
		}

		_, err = file.Write(bytes)
		if err != nil {
			return err
		}
	}

	srv.SendAndClose(&Empty{})
	return nil
}

func (fss *FileStorageServer) ReadAt(ctx context.Context, req *ReadAtRequest) (*ReadChunk, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}

	path, err := fss.getSafeFilePath(req.Path)
	if err != nil {
		return nil, err
	}

	if req.Size > MaxChunkSize {
		return nil, err
	}

	file, err := os.OpenFile(path.Render(), os.O_RDONLY, 0700)
	if err != nil {
		return nil, err
	}

	if _, err := file.Seek(int64(req.StartOffset), 0); err != nil {
		return nil, err
	}

	data := make([]byte, req.Size)

	i, err := file.Read(data)
	if err != nil {
		return nil, err
	}

	return &ReadChunk{
		Data: data[:i],
	}, nil
}

func (fss *FileStorageServer) ListDirectory(ctx context.Context, req *Empty) (*DirectoryList, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}

	var files []*DirectoryEnt

	if err := filepath.Walk(fss.Base.Render(), func(path string, finf os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if finf.IsDir() == false {
			files = append(files, &DirectoryEnt{
				Path: strings.Replace(path, fss.Base.Render(), "", 1),
				Size: finf.Size()})
		}
		return nil
	}); err != nil {
		return nil, err
	}

	return &DirectoryList{
		Results: files,
	}, nil
}
<commit_msg>reduce max chunking size dramatically<commit_after>package i9k

import (
	context "context"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/golang/protobuf/ptypes"

	"github.com/superp00t/etc"

	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

const (
	// 3 megabytes
	MaxChunkSize = 3e+6
)

type File struct {
	Path string
	*os.File
}

// FileStorageServer can be embedded to have forward compatible implementations.
type FileStorageServer struct {
	Base        etc.Path
	Fingerprint string
}

// check identity of peer
func (fss *FileStorageServer) verify(ctx context.Context) error {
	fp, err := GetPeerFingerprint(ctx)
	if err != nil {
		return err
	}

	if fp != fss.Fingerprint {
		return err
	}

	return nil
}

func (fss *FileStorageServer) getSafeFilePath(path string) (etc.Path, error) {
	subPath := etc.ParseUnixPath(path)
	if len(subPath) < 1 {
		return nil, status.Errorf(codes.InvalidArgument, "Invalid path")
	}

	if subPath[0] == "..." {
		return nil, status.Errorf(codes.InvalidArgument, "Must use relative paths")
	}

	safePath := fss.Base.GetSub(subPath)

	return safePath, nil
}

func (fss *FileStorageServer) statInfo(finf os.FileInfo) *StatResponse {
	tm, err := ptypes.TimestampProto(finf.ModTime())
	if err != nil {
		panic(err)
	}

	return &StatResponse{
		FileName:        finf.Name(),
		FileSize:        finf.Size(),
		FileMode:        uint32(finf.Mode()),
		FileModTime:     tm,
		FileIsDirectory: finf.IsDir(),
	}
}

func (fss *FileStorageServer) Stat(ctx context.Context, req *StatRequest) (*StatResponse, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}
	safePath, err := fss.getSafeFilePath(req.Path)
	if err != nil {
		return nil, err
	}

	finf, err := os.Stat(safePath.Render())
	if err != nil {
		return nil, err
	}

	if finf.Size() == 0 {
		panic("invalid size")
	}

	sr := fss.statInfo(finf)

	return sr, nil
}

func (fss *FileStorageServer) WriteAll(srv Storage_WriteAllServer) error {
	if err := fss.verify(srv.Context()); err != nil {
		return err
	}

	var file *os.File

	for {
		data, err := srv.Recv()
		if err == io.EOF {
			break
		}

		if err != nil {
			return err
		}

		if file == nil {
			fh := data.GetFileName()
			if fh == "" {
				return status.Errorf(codes.InvalidArgument, "handle should be first element in stream")
			}

			filePath, err := fss.getSafeFilePath(fh)
			if err != nil {
				return err
			}

			if _, err = os.Stat(filePath.Render()); err == nil {
				os.Remove(filePath.Render())
			}

			file, err = os.OpenFile(filePath.Render(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0700)
			if err != nil {
				panic(err)
			}
			continue
		}

		bytes := data.GetData()
		if bytes == nil {
			return status.Errorf(codes.InvalidArgument, "no data")
		}

		_, err = file.Write(bytes)
		if err != nil {
			return err
		}
	}

	srv.SendAndClose(&Empty{})
	return nil
}

func (fss *FileStorageServer) ReadAt(ctx context.Context, req *ReadAtRequest) (*ReadChunk, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}

	path, err := fss.getSafeFilePath(req.Path)
	if err != nil {
		return nil, err
	}

	if req.Size > MaxChunkSize {
		return nil, err
	}

	file, err := os.OpenFile(path.Render(), os.O_RDONLY, 0700)
	if err != nil {
		return nil, err
	}

	if _, err := file.Seek(int64(req.StartOffset), 0); err != nil {
		return nil, err
	}

	data := make([]byte, req.Size)

	i, err := file.Read(data)
	if err != nil {
		return nil, err
	}

	return &ReadChunk{
		Data: data[:i],
	}, nil
}

func (fss *FileStorageServer) ListDirectory(ctx context.Context, req *Empty) (*DirectoryList, error) {
	if err := fss.verify(ctx); err != nil {
		return nil, err
	}

	var files []*DirectoryEnt

	if err := filepath.Walk(fss.Base.Render(), func(path string, finf os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if finf.IsDir() == false {
			files = append(files, &DirectoryEnt{
				Path: strings.Replace(path, fss.Base.Render(), "", 1),
				Size: finf.Size()})
		}
		return nil
	}); err != nil {
		return nil, err
	}

	return &DirectoryList{
		Results: files,
	}, nil
}
<|endoftext|>


<commit_before>package corbel

import (
	"fmt"
	"net/http"
)

// IAMAuthConfiguration is the representation of an AuthConfiguration object used by IAM
type IAMAuthConfiguration struct {
	// Type defined the Auth Configuration type defined
	Type        string `json:"type"`
	RedirectURL string `json:"redirectUri"`
	// ClientId used for Facebook, Google and Corbel Oauth 2.0
	ClientID     string `json:"clientID,omitempty"`
	ClientSecret string `json:"clientSecret,omitempty"`
	// OAuthServerURL is the Oauth URL to use in Corbel Oauth
	OAuthServerURL string `json:"oAuthServerUrl,omitempty"`
	// CounsumerKey used for Twitter Oauth
	ConsumerKey    string `json:"consumerKey,omitempty"`
	ConsumerSecret string `json:"consumerSecret,omitempty"`
}

// IAMDomain is the representation of an Domain object used by IAM
type IAMDomain struct {
	ID                 string                          `json:"id"`
	Description        string                          `json:"description,omitempty"`
	AuthURL            string                          `json:"authUrl, omitempty"`
	AllowedDomains     string                          `json:"allowedDomains"`
	Scopes             []string                        `json:"scopes,omitempty"`
	DefaultScopes      []string                        `json:"defaultScopes,omitempty"`
	AuthConfigurations map[string]IAMAuthConfiguration `json:"authConfigurations,omitempty"`
	UserProfileFields  []string                        `json:"userProfileFields,omitempty"`
	CreatedDate        int                             `json:"createdDate,omitempty"`
	CreatedBy          string                          `json:"createdBy,omitempty"`
}

// IAMRule is the representation of a Rule for a Scope object used by IAM
type IAMRule struct {
	MediaTypes []string `json:"mediaTypes"`
	Methods    []string `json:"methods"`
	Type       string   `json:"type"`
	URI        string   `json:"uri"`
	TokenType  string   `json:"tokenType"`
}

// IAMClient is the representation of a Client object used by IAM
type IAMClient struct {
	ID                       string   `json:"id,omitempty"`
	Key                      string   `json:"key"`
	Name                     string   `json:"name"`
	Domain                   string   `json:"domain"`
	Version                  string   `json:"version,omitempty"`
	SignatureAlgorithm       string   `json:"signatureAlgorithm,omitempty"`
	Scopes                   []string `json:"scopes,omitempty"`
	ClientSideAuthentication bool     `json:"clientSideAuthentication"`
	ResetURL                 string   `json:"resetUrl,omitempty"`
	ResetNotificationID      string   `json:"resetNotificationId,omitempty"`
}

// IAMScope is the representation of a Scope object used by IAM
type IAMScope struct {
	ID         string            `json:"id"`
	Audience   string            `json:"audience"`
	Type       string            `json:"type"`
	Scopes     []string          `json:"scopes,omitempty"`
	Parameters map[string]string `json:"parameters,omitempty"`
	Rules      []IAMRule         `json:"rules,omitempty"`
}

// DomainAdd adds an Domain defined struct to the platform
func (i *IAMService) DomainAdd(domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", "/v1.0/domain/", domain)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// DomainUpdate updates an domain by using IAMDomain
func (i *IAMService) DomainUpdate(id string, domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("PUT", "iam", fmt.Sprintf("/v1.0/domain/%s", id), domain)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// DomainGet gets the desired IAMUdomain from the domain by id
func (i *IAMService) DomainGet(id string, domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/domain/%s", id), nil)
	return returnErrorHTTPInterface(i.client, req, err, domain, 200)
}

// DomainDelete deletes the desired domain from IAM by id
func (i *IAMService) DomainDelete(id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/domain/%s", id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// DomainSearch gets the desired objects in base of a search query
func (i *IAMService) DomainSearch() *Search {
	return NewSearch(i.client, "iam", "/v1.0/domain")
}

// ClientAdd adds an Client defined struct to the platform
func (i *IAMService) ClientAdd(client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", fmt.Sprintf("/v1.0/domain/%s/client/", client.Domain), client)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// ClientUpdate updates an client by using IAMClient
func (i *IAMService) ClientUpdate(id string, client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("PUT", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", client.Domain, id), client)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// ClientGet gets the desired IAMClient
func (i *IAMService) ClientGet(domainName, id string, client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", domainName, id), nil)
	return returnErrorHTTPInterface(i.client, req, err, client, 200)
}

// ClientDelete deletes the desired client from IAM by id
func (i *IAMService) ClientDelete(domainName, id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", domainName, id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// ClientSearch gets the desired objects in base of a search query
func (i *IAMService) ClientSearch(domainName string) *Search {
	return NewSearch(i.client, "iam", fmt.Sprintf("/v1.0/domain/%s/client", domainName))
}

// ScopeAdd adds an Scope defined struct to the platform
func (i *IAMService) ScopeAdd(scope *IAMScope) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", "/v1.0/scope/", scope)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// ScopeUpdate updates an scope by using IAMScope
func (i *IAMService) ScopeUpdate(scope *IAMScope) error {
	return i.ScopeAdd(scope)
}

// ScopeGet gets the desired IAMScope from the scope by id
func (i *IAMService) ScopeGet(id string, scope *IAMScope) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/scope/%s", id), nil)
	return returnErrorHTTPInterface(i.client, req, err, scope, 200)
}

// ScopeDelete deletes the desired scope from IAM by id
func (i *IAMService) ScopeDelete(id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/scope/%s", id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// ScopeSearch gets the desired objects in base of a search query
func (i *IAMService) ScopeSearch() *Search {
	return NewSearch(i.client, "iam", "/v1.0/scope")
}
<commit_msg>IAM scopes does not allow searchs<commit_after>package corbel

import (
	"fmt"
	"net/http"
)

// IAMAuthConfiguration is the representation of an AuthConfiguration object used by IAM
type IAMAuthConfiguration struct {
	// Type defined the Auth Configuration type defined
	Type        string `json:"type"`
	RedirectURL string `json:"redirectUri"`
	// ClientId used for Facebook, Google and Corbel Oauth 2.0
	ClientID     string `json:"clientID,omitempty"`
	ClientSecret string `json:"clientSecret,omitempty"`
	// OAuthServerURL is the Oauth URL to use in Corbel Oauth
	OAuthServerURL string `json:"oAuthServerUrl,omitempty"`
	// CounsumerKey used for Twitter Oauth
	ConsumerKey    string `json:"consumerKey,omitempty"`
	ConsumerSecret string `json:"consumerSecret,omitempty"`
}

// IAMDomain is the representation of an Domain object used by IAM
type IAMDomain struct {
	ID                 string                          `json:"id"`
	Description        string                          `json:"description,omitempty"`
	AuthURL            string                          `json:"authUrl, omitempty"`
	AllowedDomains     string                          `json:"allowedDomains"`
	Scopes             []string                        `json:"scopes,omitempty"`
	DefaultScopes      []string                        `json:"defaultScopes,omitempty"`
	AuthConfigurations map[string]IAMAuthConfiguration `json:"authConfigurations,omitempty"`
	UserProfileFields  []string                        `json:"userProfileFields,omitempty"`
	CreatedDate        int                             `json:"createdDate,omitempty"`
	CreatedBy          string                          `json:"createdBy,omitempty"`
}

// IAMRule is the representation of a Rule for a Scope object used by IAM
type IAMRule struct {
	MediaTypes []string `json:"mediaTypes"`
	Methods    []string `json:"methods"`
	Type       string   `json:"type"`
	URI        string   `json:"uri"`
	TokenType  string   `json:"tokenType"`
}

// IAMClient is the representation of a Client object used by IAM
type IAMClient struct {
	ID                       string   `json:"id,omitempty"`
	Key                      string   `json:"key"`
	Name                     string   `json:"name"`
	Domain                   string   `json:"domain"`
	Version                  string   `json:"version,omitempty"`
	SignatureAlgorithm       string   `json:"signatureAlgorithm,omitempty"`
	Scopes                   []string `json:"scopes,omitempty"`
	ClientSideAuthentication bool     `json:"clientSideAuthentication"`
	ResetURL                 string   `json:"resetUrl,omitempty"`
	ResetNotificationID      string   `json:"resetNotificationId,omitempty"`
}

// IAMScope is the representation of a Scope object used by IAM
type IAMScope struct {
	ID         string            `json:"id"`
	Audience   string            `json:"audience"`
	Type       string            `json:"type"`
	Scopes     []string          `json:"scopes,omitempty"`
	Parameters map[string]string `json:"parameters,omitempty"`
	Rules      []IAMRule         `json:"rules,omitempty"`
}

// DomainAdd adds an Domain defined struct to the platform
func (i *IAMService) DomainAdd(domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", "/v1.0/domain/", domain)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// DomainUpdate updates an domain by using IAMDomain
func (i *IAMService) DomainUpdate(id string, domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("PUT", "iam", fmt.Sprintf("/v1.0/domain/%s", id), domain)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// DomainGet gets the desired IAMUdomain from the domain by id
func (i *IAMService) DomainGet(id string, domain *IAMDomain) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/domain/%s", id), nil)
	return returnErrorHTTPInterface(i.client, req, err, domain, 200)
}

// DomainDelete deletes the desired domain from IAM by id
func (i *IAMService) DomainDelete(id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/domain/%s", id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// DomainSearch gets the desired objects in base of a search query
func (i *IAMService) DomainSearch() *Search {
	return NewSearch(i.client, "iam", "/v1.0/domain")
}

// ClientAdd adds an Client defined struct to the platform
func (i *IAMService) ClientAdd(client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", fmt.Sprintf("/v1.0/domain/%s/client/", client.Domain), client)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// ClientUpdate updates an client by using IAMClient
func (i *IAMService) ClientUpdate(id string, client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("PUT", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", client.Domain, id), client)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// ClientGet gets the desired IAMClient
func (i *IAMService) ClientGet(domainName, id string, client *IAMClient) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", domainName, id), nil)
	return returnErrorHTTPInterface(i.client, req, err, client, 200)
}

// ClientDelete deletes the desired client from IAM by id
func (i *IAMService) ClientDelete(domainName, id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/domain/%s/client/%s", domainName, id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}

// ClientSearch gets the desired objects in base of a search query
func (i *IAMService) ClientSearch(domainName string) *Search {
	return NewSearch(i.client, "iam", fmt.Sprintf("/v1.0/domain/%s/client", domainName))
}

// ScopeAdd adds an Scope defined struct to the platform
func (i *IAMService) ScopeAdd(scope *IAMScope) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("POST", "iam", "/v1.0/scope/", scope)
	return returnErrorHTTPSimple(i.client, req, err, 201)
}

// ScopeUpdate updates an scope by using IAMScope
func (i *IAMService) ScopeUpdate(scope *IAMScope) error {
	return i.ScopeAdd(scope)
}

// ScopeGet gets the desired IAMScope from the scope by id
func (i *IAMService) ScopeGet(id string, scope *IAMScope) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("GET", "iam", fmt.Sprintf("/v1.0/scope/%s", id), nil)
	return returnErrorHTTPInterface(i.client, req, err, scope, 200)
}

// ScopeDelete deletes the desired scope from IAM by id
func (i *IAMService) ScopeDelete(id string) error {
	var (
		req *http.Request
		err error
	)

	req, err = i.client.NewRequest("DELETE", "iam", fmt.Sprintf("/v1.0/scope/%s", id), nil)
	return returnErrorHTTPSimple(i.client, req, err, 204)
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/codegangsta/cli"

	"github.com/docker-library/go-dockerlibrary/manifest"
)

// TODO somewhere, ensure that the Docker engine we're talking to is API version 1.22+ (Docker 1.10+)
//   docker version --format '{{.Server.APIVersion}}'

var (
	configPath  string
	flagsConfig *FlagsConfig

	defaultLibrary string
	defaultCache   string

	arch                 string
	constraints          []string
	exclusiveConstraints bool

	debugFlag  = false
	noSortFlag = false

	// separated so that FlagsConfig.ApplyTo can access them
	flagEnvVars = map[string]string{
		"debug":   "BASHBREW_DEBUG",
		"arch":    "BASHBREW_ARCH",
		"config":  "BASHBREW_CONFIG",
		"library": "BASHBREW_LIBRARY",
		"cache":   "BASHBREW_CACHE",
	}
)

func initDefaultConfigPath() string {
	xdgConfig := os.Getenv("XDG_CONFIG_HOME")
	if xdgConfig == "" {
		xdgConfig = filepath.Join(os.Getenv("HOME"), ".config")
	}
	return filepath.Join(xdgConfig, "bashbrew")
}

func initDefaultCachePath() string {
	xdgCache := os.Getenv("XDG_CACHE_HOME")
	if xdgCache == "" {
		xdgCache = filepath.Join(os.Getenv("HOME"), ".cache")
	}
	return filepath.Join(xdgCache, "bashbrew")
}

func main() {
	app := cli.NewApp()
	app.Name = "bashbrew"
	app.Usage = "canonical build tool for the official images"
	app.Version = "dev"
	app.HideVersion = true
	app.EnableBashCompletion = true

	// TODO add "Description" to app and commands (for longer-form description of their functionality)

	cli.VersionFlag.Name = "version" // remove "-v" from VersionFlag
	cli.HelpFlag.Name = "help, h, ?" // add "-?" to HelpFlag
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:   "debug",
			EnvVar: flagEnvVars["debug"],
			Usage:  `enable more output (esp. all "docker build" output instead of only output on failure)`,
		},
		cli.BoolFlag{
			Name:  "no-sort",
			Usage: "do not apply any sorting, even via --build-order",
		},

		cli.StringFlag{
			Name:   "arch",
			Value:  manifest.DefaultArchitecture,
			EnvVar: flagEnvVars["arch"],
			Usage:  "the current platform architecture",
		},
		cli.StringSliceFlag{
			Name:  "constraint",
			Usage: "build constraints (see Constraints in Manifest2822Entry)",
		},
		cli.BoolFlag{
			Name:  "exclusive-constraints",
			Usage: "skip entries which do not have Constraints",
		},

		cli.StringFlag{
			Name:   "config",
			Value:  initDefaultConfigPath(),
			EnvVar: flagEnvVars["config"],
			Usage:  `where default "flags" configuration can be overridden more persistently`,
		},
		cli.StringFlag{
			Name:   "library",
			Value:  filepath.Join(os.Getenv("HOME"), "docker", "official-images", "library"),
			EnvVar: flagEnvVars["library"],
			Usage:  "where the bodies are buried",
		},
		cli.StringFlag{
			Name:   "cache",
			Value:  initDefaultCachePath(),
			EnvVar: flagEnvVars["cache"],
			Usage:  "where the git wizardry is stashed",
		},
	}

	app.Before = func(c *cli.Context) error {
		var err error

		configPath, err = filepath.Abs(c.String("config"))
		if err != nil {
			return err
		}

		flagsConfig, err = ParseFlagsConfigFile(filepath.Join(configPath, "flags"))
		if err != nil && !os.IsNotExist(err) {
			return err
		}

		return nil
	}

	subcommandBeforeFactory := func(cmd string) cli.BeforeFunc {
		return func(c *cli.Context) error {
			err := flagsConfig.ApplyTo(cmd, c)
			if err != nil {
				return err
			}

			debugFlag = c.GlobalBool("debug")
			noSortFlag = c.GlobalBool("no-sort")

			arch = c.GlobalString("arch")
			constraints = c.GlobalStringSlice("constraint")
			exclusiveConstraints = c.GlobalBool("exclusive-constraints")

			defaultLibrary, err = filepath.Abs(c.GlobalString("library"))
			if err != nil {
				return err
			}
			defaultCache, err = filepath.Abs(c.GlobalString("cache"))
			if err != nil {
				return err
			}

			return nil
		}
	}

	// define a few useful flags so their usage, etc can be consistent
	commonFlags := map[string]cli.Flag{
		"all": cli.BoolFlag{
			Name:  "all",
			Usage: "act upon all repos listed in --library",
		},
		"uniq": cli.BoolFlag{
			Name:  "uniq, unique",
			Usage: "only act upon the first tag of each entry",
		},
		"namespace": cli.StringFlag{
			Name:  "namespace",
			Usage: "a repo namespace to act upon/in",
		},
		"apply-constraints": cli.BoolFlag{
			Name:  "apply-constraints",
			Usage: "apply Constraints as if repos were building",
		},
		"depth": cli.IntFlag{
			Name:  "depth",
			Value: 0,
			Usage: "maximum number of levels to traverse (0 for unlimited)",
		},
	}

	app.Commands = []cli.Command{
		{
			Name:    "list",
			Aliases: []string{"ls"},
			Usage:   "list repo:tag combinations for a given repo",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["apply-constraints"],
				cli.BoolFlag{
					Name:  "build-order",
					Usage: "sort by the order repos would need to build (topsort)",
				},
				cli.BoolFlag{
					Name:  "repos",
					Usage: `list only repos, not repo:tag (unless "repo:tag" is explicitly specified)`,
				},
			},
			Before: subcommandBeforeFactory("list"),
			Action: cmdList,
		},
		{
			Name:  "build",
			Usage: "build (and tag) repo:tag combinations for a given repo",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
				cli.StringFlag{
					Name:  "pull",
					Value: "missing",
					Usage: `pull FROM before building (always, missing, never)`,
				},
			},
			Before: subcommandBeforeFactory("build"),
			Action: cmdBuild,
		},
		{
			Name:  "tag",
			Usage: "tag repo:tag into a namespace (especially for pushing)",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("tag"),
			Action: cmdTag,
		},
		{
			Name:  "push",
			Usage: `push namespace/repo:tag (see also "tag")`,
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("push"),
			Action: cmdPush,
		},
		{
			Name:  "put-shared",
			Usage: `updated shared tags in the registry`,
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("put-shared"),
			Action: cmdPutShared,
		},

		{
			Name: "children",
			Aliases: []string{
				"offspring",
				"descendants",
				"progeny",
			},
			Usage: `print the repos built FROM a given repo or repo:tag`,
			Flags: []cli.Flag{
				commonFlags["apply-constraints"],
				commonFlags["depth"],
			},
			Before: subcommandBeforeFactory("children"),
			Action: cmdOffspring,

			Category: "plumbing",
		},
		{
			Name: "parents",
			Aliases: []string{
				"ancestors",
				"progenitors",
			},
			Usage: `print the repos this repo or repo:tag is FROM`,
			Flags: []cli.Flag{
				commonFlags["apply-constraints"],
				commonFlags["depth"],
			},
			Before: subcommandBeforeFactory("parents"),
			Action: cmdParents,

			Category: "plumbing",
		},
		{
			Name:  "cat",
			Usage: "print manifest contents for repo or repo:tag",
			Flags: []cli.Flag{
				commonFlags["all"],
				cli.StringFlag{
					Name:  "format, f",
					Usage: "change the `FORMAT` of the output",
					Value: DefaultCatFormat,
				},
				cli.StringFlag{
					Name:  "format-file, F",
					Usage: "use the contents of `FILE` for \"--format\"",
				},
			},
			Before: subcommandBeforeFactory("cat"),
			Action: cmdCat,

			Description: `see Go's "text/template" package (https://golang.org/pkg/text/template/) for details on the syntax expected in "--format"`,

			Category: "plumbing",
		},
		{
			Name:  "from",
			Usage: "print FROM for repo:tag",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["apply-constraints"],
			},
			Before: subcommandBeforeFactory("from"),
			Action: cmdFrom,

			Category: "plumbing",
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}
<commit_msg>Add a "BASHBREW_PULL" environment variable to control the "pull policy" during build<commit_after>package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/codegangsta/cli"

	"github.com/docker-library/go-dockerlibrary/manifest"
)

// TODO somewhere, ensure that the Docker engine we're talking to is API version 1.22+ (Docker 1.10+)
//   docker version --format '{{.Server.APIVersion}}'

var (
	configPath  string
	flagsConfig *FlagsConfig

	defaultLibrary string
	defaultCache   string

	arch                 string
	constraints          []string
	exclusiveConstraints bool

	debugFlag  = false
	noSortFlag = false

	// separated so that FlagsConfig.ApplyTo can access them
	flagEnvVars = map[string]string{
		"debug":   "BASHBREW_DEBUG",
		"arch":    "BASHBREW_ARCH",
		"config":  "BASHBREW_CONFIG",
		"library": "BASHBREW_LIBRARY",
		"cache":   "BASHBREW_CACHE",
		"pull":    "BASHBREW_PULL",
	}
)

func initDefaultConfigPath() string {
	xdgConfig := os.Getenv("XDG_CONFIG_HOME")
	if xdgConfig == "" {
		xdgConfig = filepath.Join(os.Getenv("HOME"), ".config")
	}
	return filepath.Join(xdgConfig, "bashbrew")
}

func initDefaultCachePath() string {
	xdgCache := os.Getenv("XDG_CACHE_HOME")
	if xdgCache == "" {
		xdgCache = filepath.Join(os.Getenv("HOME"), ".cache")
	}
	return filepath.Join(xdgCache, "bashbrew")
}

func main() {
	app := cli.NewApp()
	app.Name = "bashbrew"
	app.Usage = "canonical build tool for the official images"
	app.Version = "dev"
	app.HideVersion = true
	app.EnableBashCompletion = true

	// TODO add "Description" to app and commands (for longer-form description of their functionality)

	cli.VersionFlag.Name = "version" // remove "-v" from VersionFlag
	cli.HelpFlag.Name = "help, h, ?" // add "-?" to HelpFlag
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:   "debug",
			EnvVar: flagEnvVars["debug"],
			Usage:  `enable more output (esp. all "docker build" output instead of only output on failure)`,
		},
		cli.BoolFlag{
			Name:  "no-sort",
			Usage: "do not apply any sorting, even via --build-order",
		},

		cli.StringFlag{
			Name:   "arch",
			Value:  manifest.DefaultArchitecture,
			EnvVar: flagEnvVars["arch"],
			Usage:  "the current platform architecture",
		},
		cli.StringSliceFlag{
			Name:  "constraint",
			Usage: "build constraints (see Constraints in Manifest2822Entry)",
		},
		cli.BoolFlag{
			Name:  "exclusive-constraints",
			Usage: "skip entries which do not have Constraints",
		},

		cli.StringFlag{
			Name:   "config",
			Value:  initDefaultConfigPath(),
			EnvVar: flagEnvVars["config"],
			Usage:  `where default "flags" configuration can be overridden more persistently`,
		},
		cli.StringFlag{
			Name:   "library",
			Value:  filepath.Join(os.Getenv("HOME"), "docker", "official-images", "library"),
			EnvVar: flagEnvVars["library"],
			Usage:  "where the bodies are buried",
		},
		cli.StringFlag{
			Name:   "cache",
			Value:  initDefaultCachePath(),
			EnvVar: flagEnvVars["cache"],
			Usage:  "where the git wizardry is stashed",
		},
	}

	app.Before = func(c *cli.Context) error {
		var err error

		configPath, err = filepath.Abs(c.String("config"))
		if err != nil {
			return err
		}

		flagsConfig, err = ParseFlagsConfigFile(filepath.Join(configPath, "flags"))
		if err != nil && !os.IsNotExist(err) {
			return err
		}

		return nil
	}

	subcommandBeforeFactory := func(cmd string) cli.BeforeFunc {
		return func(c *cli.Context) error {
			err := flagsConfig.ApplyTo(cmd, c)
			if err != nil {
				return err
			}

			debugFlag = c.GlobalBool("debug")
			noSortFlag = c.GlobalBool("no-sort")

			arch = c.GlobalString("arch")
			constraints = c.GlobalStringSlice("constraint")
			exclusiveConstraints = c.GlobalBool("exclusive-constraints")

			defaultLibrary, err = filepath.Abs(c.GlobalString("library"))
			if err != nil {
				return err
			}
			defaultCache, err = filepath.Abs(c.GlobalString("cache"))
			if err != nil {
				return err
			}

			return nil
		}
	}

	// define a few useful flags so their usage, etc can be consistent
	commonFlags := map[string]cli.Flag{
		"all": cli.BoolFlag{
			Name:  "all",
			Usage: "act upon all repos listed in --library",
		},
		"uniq": cli.BoolFlag{
			Name:  "uniq, unique",
			Usage: "only act upon the first tag of each entry",
		},
		"namespace": cli.StringFlag{
			Name:  "namespace",
			Usage: "a repo namespace to act upon/in",
		},
		"apply-constraints": cli.BoolFlag{
			Name:  "apply-constraints",
			Usage: "apply Constraints as if repos were building",
		},
		"depth": cli.IntFlag{
			Name:  "depth",
			Value: 0,
			Usage: "maximum number of levels to traverse (0 for unlimited)",
		},
	}

	app.Commands = []cli.Command{
		{
			Name:    "list",
			Aliases: []string{"ls"},
			Usage:   "list repo:tag combinations for a given repo",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["apply-constraints"],
				cli.BoolFlag{
					Name:  "build-order",
					Usage: "sort by the order repos would need to build (topsort)",
				},
				cli.BoolFlag{
					Name:  "repos",
					Usage: `list only repos, not repo:tag (unless "repo:tag" is explicitly specified)`,
				},
			},
			Before: subcommandBeforeFactory("list"),
			Action: cmdList,
		},
		{
			Name:  "build",
			Usage: "build (and tag) repo:tag combinations for a given repo",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
				cli.StringFlag{
					Name:   "pull",
					Value:  "missing",
					EnvVar: flagEnvVars["pull"],
					Usage:  `pull FROM before building (always, missing, never)`,
				},
			},
			Before: subcommandBeforeFactory("build"),
			Action: cmdBuild,
		},
		{
			Name:  "tag",
			Usage: "tag repo:tag into a namespace (especially for pushing)",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("tag"),
			Action: cmdTag,
		},
		{
			Name:  "push",
			Usage: `push namespace/repo:tag (see also "tag")`,
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("push"),
			Action: cmdPush,
		},
		{
			Name:  "put-shared",
			Usage: `updated shared tags in the registry`,
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["namespace"],
			},
			Before: subcommandBeforeFactory("put-shared"),
			Action: cmdPutShared,
		},

		{
			Name: "children",
			Aliases: []string{
				"offspring",
				"descendants",
				"progeny",
			},
			Usage: `print the repos built FROM a given repo or repo:tag`,
			Flags: []cli.Flag{
				commonFlags["apply-constraints"],
				commonFlags["depth"],
			},
			Before: subcommandBeforeFactory("children"),
			Action: cmdOffspring,

			Category: "plumbing",
		},
		{
			Name: "parents",
			Aliases: []string{
				"ancestors",
				"progenitors",
			},
			Usage: `print the repos this repo or repo:tag is FROM`,
			Flags: []cli.Flag{
				commonFlags["apply-constraints"],
				commonFlags["depth"],
			},
			Before: subcommandBeforeFactory("parents"),
			Action: cmdParents,

			Category: "plumbing",
		},
		{
			Name:  "cat",
			Usage: "print manifest contents for repo or repo:tag",
			Flags: []cli.Flag{
				commonFlags["all"],
				cli.StringFlag{
					Name:  "format, f",
					Usage: "change the `FORMAT` of the output",
					Value: DefaultCatFormat,
				},
				cli.StringFlag{
					Name:  "format-file, F",
					Usage: "use the contents of `FILE` for \"--format\"",
				},
			},
			Before: subcommandBeforeFactory("cat"),
			Action: cmdCat,

			Description: `see Go's "text/template" package (https://golang.org/pkg/text/template/) for details on the syntax expected in "--format"`,

			Category: "plumbing",
		},
		{
			Name:  "from",
			Usage: "print FROM for repo:tag",
			Flags: []cli.Flag{
				commonFlags["all"],
				commonFlags["uniq"],
				commonFlags["apply-constraints"],
			},
			Before: subcommandBeforeFactory("from"),
			Action: cmdFrom,

			Category: "plumbing",
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}
<|endoftext|>


<commit_before>// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package internal

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
)

// ParseKey converts the binary contents of a private key file
// to an *rsa.PrivateKey. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode(key)
	if block != nil {
		key = block.Bytes
	}
	parsedKey, err := x509.ParsePKCS8PrivateKey(key)
	if err != nil {
		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
		if err != nil {
			return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
		}
	}
	parsed, ok := parsedKey.(*rsa.PrivateKey)
	if !ok {
		return nil, errors.New("private key is invalid")
	}
	return parsed, nil
}
<commit_msg>oauth2: fix error message typo<commit_after>// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package internal

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
)

// ParseKey converts the binary contents of a private key file
// to an *rsa.PrivateKey. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode(key)
	if block != nil {
		key = block.Bytes
	}
	parsedKey, err := x509.ParsePKCS8PrivateKey(key)
	if err != nil {
		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
		if err != nil {
			return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err)
		}
	}
	parsed, ok := parsedKey.(*rsa.PrivateKey)
	if !ok {
		return nil, errors.New("private key is invalid")
	}
	return parsed, nil
}
<|endoftext|>


<commit_before>// Copyright 2016 The Gosl Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package io

import (
	"testing"

	"github.com/cpmech/gosl/chk"
)

func Test_basic01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("basic01")

	v0 := Atob("1")
	if !v0 {
		chk.Panic("Atob failed: v0")
	}

	v1 := Atob("true")
	if !v1 {
		chk.Panic("Atob failed: v1")
	}

	v2 := Atob("0")
	if v2 {
		chk.Panic("Atob failed: v2")
	}

	v3 := Itob(0)
	if v3 {
		chk.Panic("Itob failed: v3")
	}

	v4 := Itob(-1)
	if !v4 {
		chk.Panic("Itob failed: v4")
	}

	v5 := Btoi(true)
	if v5 != 1 {
		chk.Panic("Btoi failed: v5")
	}

	v6 := Btoi(false)
	if v6 != 0 {
		chk.Panic("Btoi failed: v6")
	}

	v7 := Btoa(true)
	if v7 != "true" {
		chk.Panic("Btoa failed: v7")
	}

	v8 := Btoa(false)
	if v8 != "false" {
		chk.Panic("Btoa failed: v8")
	}
}

func Test_basic02(tst *testing.T) {

	//verbose()
	chk.PrintTitle("basic02")

	res := IntSf("%4d", []int{1, 2, 3}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "   1    2    3")

	res = DblSf("%8.3f", []float64{1, 2, 3}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "   1.000    2.000    3.000")

	res = StrSf("%s", []string{"a", "b", "c"}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "a b c")
}
<commit_msg>Add tests for parsing text into numbers<commit_after>// Copyright 2016 The Gosl Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package io

import (
	"testing"

	"github.com/cpmech/gosl/chk"
)

func TestBasic01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("Basic01")

	if !Atob("1") {
		tst.Errorf("Atob(\"1\") should have returned true\n")
		return
	}
	if !Atob("true") {
		tst.Errorf("Atob(\"true\") should have returned true\n")
	}
	if Atob("0") {
		tst.Errorf("Atob(\"0\") should have returned false\n")
	}
	if Atob("false") {
		tst.Errorf("Atob(\"false\") should have returned false\n")
	}

	if Itob(0) {
		tst.Errorf("Itob(0) should have returned false\n")
	}
	if !Itob(-1) {
		tst.Errorf("Itob(-1) should have returned true\n")
	}
	if !Itob(+1) {
		tst.Errorf("Itob(+1) should have returned true\n")
	}

	chk.Int(tst, "true => 1", Btoi(true), 1)
	chk.Int(tst, "false => 0", Btoi(false), 0)

	chk.Int(tst, "\"123\" => 123", Atoi("123"), 123)

	chk.String(tst, Btoa(true), "true")
	chk.String(tst, Btoa(false), "false")

	chk.Float64(tst, "\"123.456\" => 123.456", 1e-15, Atof("123.456"), 123.456)
}

func TestBasic02(tst *testing.T) {

	//verbose()
	chk.PrintTitle("Parsing02. Atob panic")

	defer chk.RecoverTstPanicIsOK(tst)
	Atob("dorival")
}

func TestBasic03(tst *testing.T) {

	//verbose()
	chk.PrintTitle("Parsing03. Atoi panic")

	defer chk.RecoverTstPanicIsOK(tst)
	Atoi("dorival")
}

func TestBasic04(tst *testing.T) {

	//verbose()
	chk.PrintTitle("Parsing04. Atof panic")

	defer chk.RecoverTstPanicIsOK(tst)
	Atof("dorival")
}

func TestBasic05(tst *testing.T) {

	//verbose()
	chk.PrintTitle("Basic05. IntSf, DblSf, StrSf")

	res := IntSf("%4d", []int{1, 2, 3}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "   1    2    3")

	res = DblSf("%8.3f", []float64{1, 2, 3}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "   1.000    2.000    3.000")

	res = StrSf("%s", []string{"a", "b", "c"}) // note that an inner space is always added
	Pforan("res = %q\n", res)
	chk.String(tst, res, "a b c")
}
<|endoftext|>


<commit_before>package main

import (
	"net"
	"log"
	"bytes"
	"encoding/binary"
	"github.com/rmxymh/infra-ecosphere/utils"
	"github.com/rmxymh/infra-ecosphere/ipmi"
	"fmt"
	"github.com/rmxymh/infra-ecosphere/web"
	"github.com/jmcvetta/napping"
	"github.com/rmxymh/infra-ecosphere/bmc"
)

var EcosphereIP string = "10.0.2.2"
var EcospherePort int = 9090

func SetBootDevice(addr *net.UDPAddr, server *net.UDPConn, wrapper ipmi.IPMISessionWrapper, message ipmi.IPMIMessage, selector ipmi.IPMIChassisBootOptionParameterSelector) {
	localIP := utils.GetLocalIP(server)

	buf := bytes.NewBuffer(selector.Parameters)
	request := ipmi.IPMIChassisBootOptionBootFlags{}
	binary.Read(buf, binary.BigEndian, &request)

	// Simulate: We just dump log but do nothing here.
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_VALID != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Valid")
	}
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_PERSISTENT != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Persistent")
	} else {
		log.Println("        IPMI CHASSIS BOOT FLAG: Only on the next boot")
	}
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_BOOT_TYPE_EFI != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Boot Type = EFI")
	} else {
		log.Println("        IPMI CHASSIS BOOT FLAG: Boot Type = PC Compatible (Legacy)")
	}

	// Simulate: We just dump log but do nothing here
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_CMOS_CLEAR != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: CMOS Clear")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_LOCK_KEYBOARD != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock Keyboard")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_SCREEN_BLANK != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Screen Blank")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_LOCK_RESET != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock RESET Buttons")
	}

	// This part contains some options that we only support: PXE, CD, HDD
	//   Maybe there is another way to simulate remote device.
	device := (request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_DEVICE) >> 2

	bootdevReq := web.WebReqBootDev{}
	bootdevResp := web.WebRespBootDev{}
	baseAPI := fmt.Sprintf("http://%s:%d/api/BMCs/%s/bootdev", EcosphereIP, EcospherePort, localIP)

	switch device {
	case ipmi.BOOT_DEVICE_FORCE_PXE:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_PXE")
		bootdevReq.Device = "PXE"
		resp, err := napping.Put(baseAPI, &bootdevReq, &bootdevResp, nil)
		if err != nil {
			log.Println("Failed to call ecophsere Web API for setting bootdev: ", err.Error())
		} else if resp.Status() != 200 {
			log.Println("Failed to call ecosphere Web API for setting bootdev: ", bootdevResp.Status)
		}

	case ipmi.BOOT_DEVICE_FORCE_HDD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_HDD")
		bootdevReq.Device = "DISK"
		resp, err := napping.Put(baseAPI, &bootdevReq, &bootdevResp, nil)
		if err != nil {
			log.Println("Failed to call ecophsere Web API for setting bootdev: ", err.Error())
		} else if resp.Status() != 200 {
			log.Println("Failed to call ecosphere Web API for setting bootdev: ", bootdevResp.Status)
		}

	case ipmi.BOOT_DEVICE_FORCE_HDD_SAFE:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_HDD_SAFE")
	case ipmi.BOOT_DEVICE_FORCE_DIAG_PARTITION:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_DIAG_PARTITION")
	case ipmi.BOOT_DEVICE_FORCE_CD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_CD")
	case ipmi.BOOT_DEVICE_FORCE_BIOS:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_BIOS")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_FLOPPY:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_FLOPPY")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_MEDIA:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_MEDIA")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_CD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_CD")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_HDD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_HDD")
	}

	// Simulate: We just dump log but do nothing here.
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_LOCK_VIA_POWER != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock out (power off / sleep request) via Power Button")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_EVENT_TRAP != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Force Progress Event Trap (Only for IPMI 2.0)")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_PASSWORD_BYPASS != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: User password bypass")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_LOCK_SLEEP != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock out Sleep Button")
	}
	verbosity := (request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_FIRMWARE) >> 5
	switch verbosity {
	case ipmi.BOOT_BIOS_FIRMWARE_SYSTEM_DEFAULT:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_SYSTEM_DEFAULT")
	case ipmi.BOOT_BIOS_FIRMWARE_REQUEST_QUIET:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_REQUEST_QUIET")
	case ipmi.BOOT_BIOS_FIRMWARE_REQUEST_VERBOSE:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_REQUEST_VERBOSE")
	}
	console_redirect := (request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_CONSOLE_REDIRECT)
	switch console_redirect {
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_OCCURS_PER_BIOS_SETTING:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_OCCURS_PER_BIOS_SETTING")
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_SUPRESS_CONSOLE_IF_ENABLED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_SUPRESS_CONSOLE_IF_ENABLED")
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_REQUEST_ENABLED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_REQUEST_ENABLED")
	}

	// Simulate: We just dump log but do nothing here.
	if request.BIOSSharedMode & ipmi.BOOT_BIOS_SHARED_BITMASK_OVERRIDE != 0 {
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_BITMASK_OVERRIDE")
	}
	mux_control := request.BIOSSharedMode & ipmi.BOOT_BIOS_SHARED_BITMASK_MUX_CONTROL_OVERRIDE
	switch mux_control {
	case ipmi.BOOT_BIOS_SHARED_MUX_RECOMMENDED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_RECOMMENDED")
	case ipmi.BOOT_BIOS_SHARED_MUX_TO_SYSTEM:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_TO_SYSTEM")
	case ipmi.BOOT_BIOS_SHARED_MUX_TO_BMC:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_TO_BMC")
	}

	ipmi.SendIPMIChassisSetBootOptionResponseBack(addr, server, wrapper, message);
}

func DoPowerOperationRestCall(powerOpReq web.WebReqPowerOp, bmcIP string) (powerOpResp web.WebRespPowerOp, err error) {
	baseAPI := fmt.Sprintf("http://%s:%d/api/BMCs/%s/power", EcosphereIP, EcospherePort, bmcIP)

	resp, err := napping.Put(baseAPI, &powerOpReq, &powerOpResp, nil)
	if err != nil {
		log.Println("Failed to call ecophsere Web API for power operation: ", err.Error())
	} else if resp.Status() != 200 {
		log.Println("Failed to call ecosphere Web API for power operation: ", powerOpResp.Status)
	}

	return powerOpResp, err
}

func HandleIPMIChassisControl(addr *net.UDPAddr, server *net.UDPConn, wrapper ipmi.IPMISessionWrapper, message ipmi.IPMIMessage) {
	buf := bytes.NewBuffer(message.Data)
	request := ipmi.IPMIChassisControlRequest{}
	binary.Read(buf, binary.BigEndian, &request)

	session, ok := ipmi.GetSession(wrapper.SessionId)
	if ! ok {
		log.Printf("Unable to find session 0x%08x\n", wrapper.SessionId)
	} else {
		bmcUser := session.User
		code := ipmi.GetAuthenticationCode(wrapper.AuthenticationType, bmcUser.Password, wrapper.SessionId, message, wrapper.SequenceNumber)
		if bytes.Compare(wrapper.AuthenticationCode[:], code[:]) == 0 {
			log.Println("      IPMI Authentication Pass.")
		} else {
			log.Println("      IPMI Authentication Failed.")
		}

		localIP := utils.GetLocalIP(server)
		powerOpReq := web.WebReqPowerOp{
			Operation: "ON",
		}

		var err error = nil
		_, ok := bmc.GetBMC(net.ParseIP(localIP))
		if ! ok {
			log.Printf("BMC %s is not found\n", localIP)
		} else {
			switch request.ChassisControl {
			case ipmi.CHASSIS_CONTROL_POWER_DOWN:
				powerOpReq.Operation = "OFF"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_POWER_UP:
				powerOpReq.Operation = "ON"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_POWER_CYCLE:
				powerOpReq.Operation = "CYCLE"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_HARD_RESET:
				powerOpReq.Operation = "RESET"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)
			case ipmi.CHASSIS_CONTROL_PULSE:
			// do nothing
			case ipmi.CHASSIS_CONTROL_POWER_SOFT:
				powerOpReq.Operation = "SOFT"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)
			}

			session.Inc()

			responseWrapper, responseMessage := ipmi.BuildResponseMessageTemplate(wrapper, message, (ipmi.IPMI_NETFN_CHASSIS | ipmi.IPMI_NETFN_RESPONSE), ipmi.IPMI_CMD_CHASSIS_CONTROL)
			if err != nil {
				responseMessage.CompletionCode = 0xD3
			}

			responseWrapper.SessionId = wrapper.SessionId
			responseWrapper.SequenceNumber = session.RemoteSessionSequenceNumber
			rmcp := ipmi.BuildUpRMCPForIPMI()

			obuf := bytes.Buffer{}
			ipmi.SerializeRMCP(&obuf, rmcp)
			ipmi.SerializeIPMI(&obuf, responseWrapper, responseMessage, bmcUser.Password)
			server.WriteToUDP(obuf.Bytes(), addr)
		}
	}
}<commit_msg>Change the type of SetBootDeviceFlag<commit_after>package main

import (
	"net"
	"log"
	"bytes"
	"encoding/binary"
	"github.com/rmxymh/infra-ecosphere/utils"
	"github.com/rmxymh/infra-ecosphere/ipmi"
	"fmt"
	"github.com/rmxymh/infra-ecosphere/web"
	"github.com/jmcvetta/napping"
	"github.com/rmxymh/infra-ecosphere/bmc"
)

var EcosphereIP string = "10.0.2.2"
var EcospherePort int = 9090

func SetBootDevice(addr *net.UDPAddr, server *net.UDPConn, wrapper ipmi.IPMISessionWrapper, message ipmi.IPMIMessage, selector ipmi.IPMIChassisBootOptionParameterSelector) {
	localIP := utils.GetLocalIP(server)

	buf := bytes.NewBuffer(selector.Parameters)
	request := ipmi.IPMIChassisSetBootOptionBootFlags{}
	binary.Read(buf, binary.BigEndian, &request)

	// Simulate: We just dump log but do nothing here.
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_VALID != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Valid")
	}
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_PERSISTENT != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Persistent")
	} else {
		log.Println("        IPMI CHASSIS BOOT FLAG: Only on the next boot")
	}
	if request.BootParam & ipmi.BOOT_PARAM_BITMASK_BOOT_TYPE_EFI != 0 {
		log.Println("        IPMI CHASSIS BOOT FLAG: Boot Type = EFI")
	} else {
		log.Println("        IPMI CHASSIS BOOT FLAG: Boot Type = PC Compatible (Legacy)")
	}

	// Simulate: We just dump log but do nothing here
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_CMOS_CLEAR != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: CMOS Clear")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_LOCK_KEYBOARD != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock Keyboard")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_SCREEN_BLANK != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Screen Blank")
	}
	if request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_LOCK_RESET != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock RESET Buttons")
	}

	// This part contains some options that we only support: PXE, CD, HDD
	//   Maybe there is another way to simulate remote device.
	device := (request.BootDevice & ipmi.BOOT_DEVICE_BITMASK_DEVICE) >> 2

	bootdevReq := web.WebReqBootDev{}
	bootdevResp := web.WebRespBootDev{}
	baseAPI := fmt.Sprintf("http://%s:%d/api/BMCs/%s/bootdev", EcosphereIP, EcospherePort, localIP)

	switch device {
	case ipmi.BOOT_DEVICE_FORCE_PXE:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_PXE")
		bootdevReq.Device = "PXE"
		resp, err := napping.Put(baseAPI, &bootdevReq, &bootdevResp, nil)
		if err != nil {
			log.Println("Failed to call ecophsere Web API for setting bootdev: ", err.Error())
		} else if resp.Status() != 200 {
			log.Println("Failed to call ecosphere Web API for setting bootdev: ", bootdevResp.Status)
		}

	case ipmi.BOOT_DEVICE_FORCE_HDD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_HDD")
		bootdevReq.Device = "DISK"
		resp, err := napping.Put(baseAPI, &bootdevReq, &bootdevResp, nil)
		if err != nil {
			log.Println("Failed to call ecophsere Web API for setting bootdev: ", err.Error())
		} else if resp.Status() != 200 {
			log.Println("Failed to call ecosphere Web API for setting bootdev: ", bootdevResp.Status)
		}

	case ipmi.BOOT_DEVICE_FORCE_HDD_SAFE:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_HDD_SAFE")
	case ipmi.BOOT_DEVICE_FORCE_DIAG_PARTITION:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_DIAG_PARTITION")
	case ipmi.BOOT_DEVICE_FORCE_CD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_CD")
	case ipmi.BOOT_DEVICE_FORCE_BIOS:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_BIOS")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_FLOPPY:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_FLOPPY")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_MEDIA:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_MEDIA")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_CD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_CD")
	case ipmi.BOOT_DEVICE_FORCE_REMOTE_HDD:
		log.Println("        IPMI CHASSIS BOOT DEVICE: BOOT_DEVICE_FORCE_REMOTE_HDD")
	}

	// Simulate: We just dump log but do nothing here.
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_LOCK_VIA_POWER != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock out (power off / sleep request) via Power Button")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_EVENT_TRAP != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Force Progress Event Trap (Only for IPMI 2.0)")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_PASSWORD_BYPASS != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: User password bypass")
	}
	if request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_LOCK_SLEEP != 0 {
		log.Println("        IPMI CHASSIS BOOT DEVICE: Lock out Sleep Button")
	}
	verbosity := (request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_FIRMWARE) >> 5
	switch verbosity {
	case ipmi.BOOT_BIOS_FIRMWARE_SYSTEM_DEFAULT:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_SYSTEM_DEFAULT")
	case ipmi.BOOT_BIOS_FIRMWARE_REQUEST_QUIET:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_REQUEST_QUIET")
	case ipmi.BOOT_BIOS_FIRMWARE_REQUEST_VERBOSE:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_FIRMWARE_REQUEST_VERBOSE")
	}
	console_redirect := (request.BIOSVerbosity & ipmi.BOOT_BIOS_BITMASK_CONSOLE_REDIRECT)
	switch console_redirect {
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_OCCURS_PER_BIOS_SETTING:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_OCCURS_PER_BIOS_SETTING")
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_SUPRESS_CONSOLE_IF_ENABLED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_SUPRESS_CONSOLE_IF_ENABLED")
	case ipmi.BOOT_BIOS_CONSOLE_REDIRECT_REQUEST_ENABLED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_CONSOLE_REDIRECT_REQUEST_ENABLED")
	}

	// Simulate: We just dump log but do nothing here.
	if request.BIOSSharedMode & ipmi.BOOT_BIOS_SHARED_BITMASK_OVERRIDE != 0 {
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_BITMASK_OVERRIDE")
	}
	mux_control := request.BIOSSharedMode & ipmi.BOOT_BIOS_SHARED_BITMASK_MUX_CONTROL_OVERRIDE
	switch mux_control {
	case ipmi.BOOT_BIOS_SHARED_MUX_RECOMMENDED:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_RECOMMENDED")
	case ipmi.BOOT_BIOS_SHARED_MUX_TO_SYSTEM:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_TO_SYSTEM")
	case ipmi.BOOT_BIOS_SHARED_MUX_TO_BMC:
		log.Println("        IPMI CHASSIS BOOT BIOS: BOOT_BIOS_SHARED_MUX_TO_BMC")
	}

	ipmi.SendIPMIChassisSetBootOptionResponseBack(addr, server, wrapper, message);
}

func DoPowerOperationRestCall(powerOpReq web.WebReqPowerOp, bmcIP string) (powerOpResp web.WebRespPowerOp, err error) {
	baseAPI := fmt.Sprintf("http://%s:%d/api/BMCs/%s/power", EcosphereIP, EcospherePort, bmcIP)

	resp, err := napping.Put(baseAPI, &powerOpReq, &powerOpResp, nil)
	if err != nil {
		log.Println("Failed to call ecophsere Web API for power operation: ", err.Error())
	} else if resp.Status() != 200 {
		log.Println("Failed to call ecosphere Web API for power operation: ", powerOpResp.Status)
	}

	return powerOpResp, err
}

func HandleIPMIChassisControl(addr *net.UDPAddr, server *net.UDPConn, wrapper ipmi.IPMISessionWrapper, message ipmi.IPMIMessage) {
	buf := bytes.NewBuffer(message.Data)
	request := ipmi.IPMIChassisControlRequest{}
	binary.Read(buf, binary.BigEndian, &request)

	session, ok := ipmi.GetSession(wrapper.SessionId)
	if ! ok {
		log.Printf("Unable to find session 0x%08x\n", wrapper.SessionId)
	} else {
		bmcUser := session.User
		code := ipmi.GetAuthenticationCode(wrapper.AuthenticationType, bmcUser.Password, wrapper.SessionId, message, wrapper.SequenceNumber)
		if bytes.Compare(wrapper.AuthenticationCode[:], code[:]) == 0 {
			log.Println("      IPMI Authentication Pass.")
		} else {
			log.Println("      IPMI Authentication Failed.")
		}

		localIP := utils.GetLocalIP(server)
		powerOpReq := web.WebReqPowerOp{
			Operation: "ON",
		}

		var err error = nil
		_, ok := bmc.GetBMC(net.ParseIP(localIP))
		if ! ok {
			log.Printf("BMC %s is not found\n", localIP)
		} else {
			switch request.ChassisControl {
			case ipmi.CHASSIS_CONTROL_POWER_DOWN:
				powerOpReq.Operation = "OFF"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_POWER_UP:
				powerOpReq.Operation = "ON"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_POWER_CYCLE:
				powerOpReq.Operation = "CYCLE"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)

			case ipmi.CHASSIS_CONTROL_HARD_RESET:
				powerOpReq.Operation = "RESET"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)
			case ipmi.CHASSIS_CONTROL_PULSE:
			// do nothing
			case ipmi.CHASSIS_CONTROL_POWER_SOFT:
				powerOpReq.Operation = "SOFT"
				_, err = DoPowerOperationRestCall(powerOpReq, localIP)
			}

			session.Inc()

			responseWrapper, responseMessage := ipmi.BuildResponseMessageTemplate(wrapper, message, (ipmi.IPMI_NETFN_CHASSIS | ipmi.IPMI_NETFN_RESPONSE), ipmi.IPMI_CMD_CHASSIS_CONTROL)
			if err != nil {
				responseMessage.CompletionCode = 0xD3
			}

			responseWrapper.SessionId = wrapper.SessionId
			responseWrapper.SequenceNumber = session.RemoteSessionSequenceNumber
			rmcp := ipmi.BuildUpRMCPForIPMI()

			obuf := bytes.Buffer{}
			ipmi.SerializeRMCP(&obuf, rmcp)
			ipmi.SerializeIPMI(&obuf, responseWrapper, responseMessage, bmcUser.Password)
			server.WriteToUDP(obuf.Bytes(), addr)
		}
	}
}<|endoftext|>


<commit_before>// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license

package utils

import (
	"crypto/tls"
	"errors"
	"net"
	"strings"
	"sync"
	"time"
)

// TODO: handle PROXY protocol v2 (the binary protocol)

const (
	// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
	// "a 108-byte buffer is always enough to store all the line and a trailing zero
	// for string processing."
	maxProxyLineLen = 107
)

var (
	ErrBadProxyLine = errors.New("invalid PROXY line")
	// TODO(golang/go#4373): replace this with the stdlib ErrNetClosing
	ErrNetClosing = errors.New("use of closed network connection")
)

// ListenerConfig is all the information about how to process
// incoming IRC connections on a listener.
type ListenerConfig struct {
	TLSConfig     *tls.Config
	ProxyDeadline time.Duration
	RequireProxy  bool
	// these are just metadata for easier tracking,
	// they are not used by ReloadableListener:
	Tor       bool
	STSOnly   bool
	WebSocket bool
}

// read a PROXY line one byte at a time, to ensure we don't read anything beyond
// that into a buffer, which would break the TLS handshake
func readRawProxyLine(conn net.Conn, deadline time.Duration) (result string) {
	// normally this is covered by ping timeouts, but we're doing this outside
	// of the normal client goroutine:
	conn.SetDeadline(time.Now().Add(deadline))
	defer conn.SetDeadline(time.Time{})

	var buf [maxProxyLineLen]byte
	oneByte := make([]byte, 1)
	i := 0
	for i < maxProxyLineLen {
		n, err := conn.Read(oneByte)
		if err != nil {
			return
		} else if n == 1 {
			buf[i] = oneByte[0]
			if buf[i] == '\n' {
				candidate := string(buf[0 : i+1])
				if strings.HasPrefix(candidate, "PROXY") {
					return candidate
				} else {
					return
				}
			}
			i += 1
		}
	}

	// no \r\n, fail out
	return
}

// ParseProxyLine parses a PROXY protocol (v1) line and returns the remote IP.
func ParseProxyLine(line string) (ip net.IP, err error) {
	params := strings.Fields(line)
	if len(params) != 6 || params[0] != "PROXY" {
		return nil, ErrBadProxyLine
	}
	ip = net.ParseIP(params[2])
	if ip == nil {
		return nil, ErrBadProxyLine
	}
	return ip.To16(), nil
}

/// WrappedConn is a net.Conn with some additional data stapled to it;
// the proxied IP, if one was read via the PROXY protocol, and the listener
// configuration.
type WrappedConn struct {
	net.Conn
	ProxiedIP net.IP
	Config    ListenerConfig
	// Secure indicates whether we believe the connection between us and the client
	// was secure against interception and modification (including all proxies):
	Secure bool
}

// ReloadableListener is a wrapper for net.Listener that allows reloading
// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
type ReloadableListener struct {
	// TODO: make this lock-free
	sync.Mutex
	realListener net.Listener
	config       ListenerConfig
	isClosed     bool
}

func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
	return &ReloadableListener{
		realListener: realListener,
		config:       config,
	}
}

func (rl *ReloadableListener) Reload(config ListenerConfig) {
	rl.Lock()
	rl.config = config
	rl.Unlock()
}

func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
	conn, err = rl.realListener.Accept()

	rl.Lock()
	config := rl.config
	isClosed := rl.isClosed
	rl.Unlock()

	if isClosed {
		if err == nil {
			conn.Close()
		}
		err = ErrNetClosing
	}
	if err != nil {
		return nil, err
	}

	var proxiedIP net.IP
	if config.RequireProxy {
		// this will occur synchronously on the goroutine calling Accept(),
		// but that's OK because this listener *requires* a PROXY line,
		// therefore it must be used with proxies that always send the line
		// and we won't get slowloris'ed waiting for the client response
		proxyLine := readRawProxyLine(conn, config.ProxyDeadline)
		proxiedIP, err = ParseProxyLine(proxyLine)
		if err != nil {
			conn.Close()
			return nil, err
		}
	}

	if config.TLSConfig != nil {
		conn = tls.Server(conn, config.TLSConfig)
	}

	return &WrappedConn{
		Conn:      conn,
		ProxiedIP: proxiedIP,
		Config:    config,
	}, nil
}

func (rl *ReloadableListener) Close() error {
	rl.Lock()
	rl.isClosed = true
	rl.Unlock()

	return rl.realListener.Close()
}

func (rl *ReloadableListener) Addr() net.Addr {
	return rl.realListener.Addr()
}
<commit_msg>fix (*http.Server).Serve() exiting on ErrBadProxyLine<commit_after>// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license

package utils

import (
	"crypto/tls"
	"errors"
	"net"
	"strings"
	"sync"
	"time"
)

// TODO: handle PROXY protocol v2 (the binary protocol)

const (
	// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
	// "a 108-byte buffer is always enough to store all the line and a trailing zero
	// for string processing."
	maxProxyLineLen = 107
)

// XXX implement net.Error with a Temporary() method that returns true;
// otherwise, ErrBadProxyLine will cause (*http.Server).Serve() to exit
type proxyLineError struct{}

func (p *proxyLineError) Error() string {
	return "invalid PROXY line"
}

func (p *proxyLineError) Timeout() bool {
	return false
}

func (p *proxyLineError) Temporary() bool {
	return true
}

var (
	ErrBadProxyLine error = &proxyLineError{}
	// TODO(golang/go#4373): replace this with the stdlib ErrNetClosing
	ErrNetClosing = errors.New("use of closed network connection")
)

// ListenerConfig is all the information about how to process
// incoming IRC connections on a listener.
type ListenerConfig struct {
	TLSConfig     *tls.Config
	ProxyDeadline time.Duration
	RequireProxy  bool
	// these are just metadata for easier tracking,
	// they are not used by ReloadableListener:
	Tor       bool
	STSOnly   bool
	WebSocket bool
}

// read a PROXY line one byte at a time, to ensure we don't read anything beyond
// that into a buffer, which would break the TLS handshake
func readRawProxyLine(conn net.Conn, deadline time.Duration) (result string) {
	// normally this is covered by ping timeouts, but we're doing this outside
	// of the normal client goroutine:
	conn.SetDeadline(time.Now().Add(deadline))
	defer conn.SetDeadline(time.Time{})

	var buf [maxProxyLineLen]byte
	oneByte := make([]byte, 1)
	i := 0
	for i < maxProxyLineLen {
		n, err := conn.Read(oneByte)
		if err != nil {
			return
		} else if n == 1 {
			buf[i] = oneByte[0]
			if buf[i] == '\n' {
				candidate := string(buf[0 : i+1])
				if strings.HasPrefix(candidate, "PROXY") {
					return candidate
				} else {
					return
				}
			}
			i += 1
		}
	}

	// no \r\n, fail out
	return
}

// ParseProxyLine parses a PROXY protocol (v1) line and returns the remote IP.
func ParseProxyLine(line string) (ip net.IP, err error) {
	params := strings.Fields(line)
	if len(params) != 6 || params[0] != "PROXY" {
		return nil, ErrBadProxyLine
	}
	ip = net.ParseIP(params[2])
	if ip == nil {
		return nil, ErrBadProxyLine
	}
	return ip.To16(), nil
}

/// WrappedConn is a net.Conn with some additional data stapled to it;
// the proxied IP, if one was read via the PROXY protocol, and the listener
// configuration.
type WrappedConn struct {
	net.Conn
	ProxiedIP net.IP
	Config    ListenerConfig
	// Secure indicates whether we believe the connection between us and the client
	// was secure against interception and modification (including all proxies):
	Secure bool
}

// ReloadableListener is a wrapper for net.Listener that allows reloading
// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
type ReloadableListener struct {
	// TODO: make this lock-free
	sync.Mutex
	realListener net.Listener
	config       ListenerConfig
	isClosed     bool
}

func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
	return &ReloadableListener{
		realListener: realListener,
		config:       config,
	}
}

func (rl *ReloadableListener) Reload(config ListenerConfig) {
	rl.Lock()
	rl.config = config
	rl.Unlock()
}

func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
	conn, err = rl.realListener.Accept()

	rl.Lock()
	config := rl.config
	isClosed := rl.isClosed
	rl.Unlock()

	if isClosed {
		if err == nil {
			conn.Close()
		}
		err = ErrNetClosing
	}
	if err != nil {
		return nil, err
	}

	var proxiedIP net.IP
	if config.RequireProxy {
		// this will occur synchronously on the goroutine calling Accept(),
		// but that's OK because this listener *requires* a PROXY line,
		// therefore it must be used with proxies that always send the line
		// and we won't get slowloris'ed waiting for the client response
		proxyLine := readRawProxyLine(conn, config.ProxyDeadline)
		proxiedIP, err = ParseProxyLine(proxyLine)
		if err != nil {
			conn.Close()
			return nil, err
		}
	}

	if config.TLSConfig != nil {
		conn = tls.Server(conn, config.TLSConfig)
	}

	return &WrappedConn{
		Conn:      conn,
		ProxiedIP: proxiedIP,
		Config:    config,
	}, nil
}

func (rl *ReloadableListener) Close() error {
	rl.Lock()
	rl.isClosed = true
	rl.Unlock()

	return rl.realListener.Close()
}

func (rl *ReloadableListener) Addr() net.Addr {
	return rl.realListener.Addr()
}
<|endoftext|>