diff --git a/builder/xenserver/common/workaround/step_sniffing_http.go b/builder/xenserver/common/workaround/step_sniffing_http.go new file mode 100644 index 0000000..96c2ef5 --- /dev/null +++ b/builder/xenserver/common/workaround/step_sniffing_http.go @@ -0,0 +1,177 @@ +package workaround + +// TODO Replace with sdk once https://github.com/hashicorp/packer-plugin-sdk/pull/80 is merged. + +import ( + "context" + "fmt" + "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" + "log" + "net/http" + "os" + "path" + "sort" + + "github.com/hashicorp/packer-plugin-sdk/didyoumean" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/net" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" +) + +func HTTPServerFromHTTPConfig(cfg *commonsteps.HTTPConfig) *StepHTTPServer { + server := StepHTTPServer{ + HTTPDir: cfg.HTTPDir, + HTTPContent: cfg.HTTPContent, + HTTPPortMin: cfg.HTTPPortMin, + HTTPPortMax: cfg.HTTPPortMax, + HTTPAddress: cfg.HTTPAddress, + } + + server.httpWrapper = server.wrapper() + + server.AddCallback(logRequest) + + return &server +} + +func logRequest(r *http.Request) { + log.Printf("http_server: hit %s - \"%s HTTP/%d.%d %s\"", r.RemoteAddr, r.Method, r.ProtoMajor, r.ProtoMinor, r.URL.Path) +} + +// This step creates and runs the HTTP server that is serving files from the +// directory specified by the 'http_directory` configuration parameter in the +// template. +// +// Uses: +// ui packersdk.Ui +// +// Produces: +// http_port int - The port the HTTP server started on. +type StepHTTPServer struct { + HTTPDir string + HTTPContent map[string]string + HTTPPortMin int + HTTPPortMax int + HTTPAddress string + + httpWrapper HTTPWrapper + l *net.Listener +} + +func (s *StepHTTPServer) AddCallback(callback func(r *http.Request)) { + s.httpWrapper.Callbacks = append(s.httpWrapper.Callbacks, callback) +} + +func (s *StepHTTPServer) wrapper() HTTPWrapper { + if s.HTTPDir != "" { + return HTTPWrapper{ + HTTPHandler: http.FileServer(http.Dir(s.HTTPDir)), + } + } + + return HTTPWrapper{ + HTTPHandler: MapServer(s.HTTPContent), + } +} + +type HTTPWrapper struct { + HTTPHandler http.Handler + Callbacks []func(r *http.Request) +} + +func (s HTTPWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + for _, callback := range s.Callbacks { + callback(r) + } + + s.HTTPHandler.ServeHTTP(w, r) +} + +type MapServer map[string]string + +func (s MapServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := path.Clean(r.URL.Path) + content, found := s[path] + if !found { + paths := make([]string, 0, len(s)) + for k := range s { + paths = append(paths, k) + } + sort.Strings(paths) + err := fmt.Sprintf("%s not found.", path) + if sug := didyoumean.NameSuggestion(path, paths); sug != "" { + err += fmt.Sprintf(" Did you mean %q?", sug) + } + + http.Error(w, err, http.StatusNotFound) + return + } + + if _, err := w.Write([]byte(content)); err != nil { + // log err in case the file couldn't be 100% transferred for example. + log.Printf("http_content serve error: %v", err) + } +} + +func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packersdk.Ui) + + if s.HTTPDir == "" && len(s.HTTPContent) == 0 { + state.Put("http_port", 0) + return multistep.ActionContinue + } + + if s.HTTPDir != "" { + if _, err := os.Stat(s.HTTPDir); err != nil { + err := fmt.Errorf("Error finding %q: %s", s.HTTPDir, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + // Find an available TCP port for our HTTP server + var err error + s.l, err = net.ListenRangeConfig{ + Min: s.HTTPPortMin, + Max: s.HTTPPortMax, + Addr: s.HTTPAddress, + Network: "tcp", + }.Listen(ctx) + + if err != nil { + err := fmt.Errorf("Error finding port: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Starting HTTP server on port %d", s.l.Port)) + + // Start the HTTP server and run it in the background + server := &http.Server{Addr: "", Handler: s.httpWrapper} + go server.Serve(s.l) + + // Save the address into the state so it can be accessed in the future + state.Put("http_port", s.l.Port) + + return multistep.ActionContinue +} + +func (s *StepHTTPServer) Cleanup(state multistep.StateBag) { + if s.l != nil { + ui := state.Get("ui").(packersdk.Ui) + + // Close the listener so that the HTTP server stops + if err := s.l.Close(); err != nil { + err = fmt.Errorf("Failed closing http server on port %d: %w", s.l.Port, err) + ui.Error(err.Error()) + // Here this error should be shown to the UI but it won't + // specifically stop Packer from terminating successfully. It could + // cause a "Listen leak" if it happenned a lot. Though Listen will + // try other ports if one is already used. In the case we want to + // Listen on only one port, the next Listen call could fail or be + // longer than expected. + } + } +} diff --git a/builder/xenserver/iso/builder.go b/builder/xenserver/iso/builder.go index ad87d7a..080c59e 100644 --- a/builder/xenserver/iso/builder.go +++ b/builder/xenserver/iso/builder.go @@ -5,7 +5,11 @@ import ( "errors" artifact2 "github.com/xenserver/packer-builder-xenserver/builder/xenserver/common/artifact" steps2 "github.com/xenserver/packer-builder-xenserver/builder/xenserver/common/steps" + "github.com/xenserver/packer-builder-xenserver/builder/xenserver/common/workaround" "github.com/xenserver/packer-builder-xenserver/builder/xenserver/common/xen" + "log" + "net" + "net/http" "path" "github.com/hashicorp/hcl/v2/hcldec" @@ -47,6 +51,21 @@ func (self *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (p httpReqChan := make(chan string, 1) + httpServerStep := workaround.HTTPServerFromHTTPConfig(&self.config.HTTPConfig) + httpServerStep.AddCallback(func(req *http.Request) { + log.Printf("HTTP: %s %s %s", req.RemoteAddr, req.Method, req.URL) + ip, _, err := net.SplitHostPort(req.RemoteAddr) + + if err == nil && ip != "" { + select { + case httpReqChan <- ip: + log.Printf("Remembering remote address '%s'", ip) + default: + // if ch is already full, don't block waiting to send the address, just drop it + } + } + }) + //Build the steps download_steps := []multistep.Step{ &commonsteps.StepDownload{ @@ -123,7 +142,7 @@ func (self *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (p new(steps2.StepSetVmHostSshAddress), new(steps2.StepHTTPIPDiscover), &steps2.StepCreateProxy{}, - commonsteps.HTTPServerFromHTTPConfig(&self.config.HTTPConfig), + httpServerStep, new(steps2.StepBootWait), &steps2.StepTypeBootCommand{ Ctx: *self.config.GetInterpContext(), diff --git a/builder/xenserver/xva/builder.go b/builder/xenserver/xva/builder.go index 0794fad..95af2f4 100644 --- a/builder/xenserver/xva/builder.go +++ b/builder/xenserver/xva/builder.go @@ -42,7 +42,7 @@ func (self *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (p //Share state between the other steps using a statebag state := new(multistep.BasicStateBag) state.Put("client", c) - // state.Put("config", self.config) + state.Put("config", self.config) state.Put("commonconfig", self.config.CommonConfig) state.Put("hook", hook) state.Put("ui", ui)