| package iso |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import ( |
| "bufio" |
| "bytes" |
| "database/sql" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "github.com/jmoiron/sqlx" |
| ) |
| |
| const ( |
| mkisofsBin = "mkisofs" // name of the binary that's used in the default case to generate an ISO |
| ) |
| |
| // newStreamISOCmd returns a instantiated streamISOCmd. The given ksDir |
| // is expected to be the root directory containing the kickstarter files |
| // of the desired OS. It will detect a custom `generate` executable if present, |
| // otherwise will use the default `mkisofs` command. |
| func newStreamISOCmd(ksDir string) (*streamISOCmd, error) { |
| var s streamISOCmd |
| |
| if customExec := customGenISOPath(ksDir); customExec != "" { |
| // The custom script must accept a single argument: The path |
| // where it will write the ISO. Here we create a temporary |
| // directory for this purpose. The cleanup method is responsible |
| // for removing it. |
| tmpDir, err := ioutil.TempDir("", "genISO") |
| if err != nil { |
| return nil, err |
| } |
| s.isoDest = filepath.Join(tmpDir, "tmp.iso") |
| |
| s.cmdType = "custom" |
| s.cmd = exec.Command(customExec, s.isoDest) |
| |
| return &s, nil |
| } |
| |
| s.cmdType = "default" |
| s.cmd = exec.Command( |
| mkisofsBin, |
| "-joliet-long", |
| "-input-charset", "utf-8", |
| "-b", "isolinux/isolinux.bin", |
| "-c", "isolinux/boot.cat", |
| "-no-emul-boot", |
| "-boot-load-size", "4", |
| "-boot-info-table", |
| "-R", |
| "-J", |
| "-v", |
| "-T", |
| ksDir, |
| ) |
| |
| return &s, nil |
| } |
| |
| // streamISOCmd encapsulate the logic for executing the ISO |
| // generation command. |
| type streamISOCmd struct { |
| cmd *exec.Cmd |
| cmdType string // Description of command: "default" or "custom" |
| |
| // If empty, then cmd writes to STDOUT. Othewrise, cmd writes the |
| // ISO to this path. |
| isoDest string |
| } |
| |
| // String returns the command that the stream method |
| // will execute. |
| func (s *streamISOCmd) String() string { |
| // Note: Go 1.13 adds exec.Cmd#String method |
| return strings.Join(s.cmd.Args, " ") |
| } |
| |
| // cleanup should be defered after calling newStreamISOCmd. |
| // It removes any temporary resources created. If the command |
| // doesn't need any cleanup, this is a no-op. |
| func (s *streamISOCmd) cleanup() error { |
| if s.isoDest == "" { |
| return nil |
| } |
| return os.RemoveAll(filepath.Dir(s.isoDest)) |
| } |
| |
| // stream writes to w the ISO data. Callers should |
| // always use this method and not the other more |
| // specific stream methods. |
| func (s *streamISOCmd) stream(w io.Writer) error { |
| if s.isoDest != "" { |
| return s.streamFromFile(w) |
| } |
| |
| return s.streamStdout(w) |
| } |
| |
| // streamStdout invokes the command and pipes its STDOUT |
| // to w. |
| func (s *streamISOCmd) streamStdout(w io.Writer) error { |
| var stderr bytes.Buffer |
| s.cmd.Stdout = w |
| s.cmd.Stderr = &stderr |
| if err := s.cmd.Run(); err != nil { |
| return fmt.Errorf("%v: %s", err, &stderr) |
| } |
| return nil |
| } |
| |
| // streamFromFile invokes the command and expects the ISO |
| // to be written to isoDest. It then copies the contents |
| // of that file to w. |
| func (s *streamISOCmd) streamFromFile(w io.Writer) error { |
| var stderr bytes.Buffer |
| s.cmd.Stderr = &stderr |
| if err := s.cmd.Run(); err != nil { |
| return fmt.Errorf("%v: %s", err, &stderr) |
| } |
| |
| isoFd, err := os.Open(s.isoDest) |
| if err != nil { |
| return err |
| } |
| defer isoFd.Close() |
| |
| _, err = io.Copy(w, bufio.NewReader(isoFd)) |
| return err |
| } |
| |
| // customGenISOPath returns the complete path to an alternative executable |
| // for generating the ISO. If not found, an empty string is returned. |
| // In order to be valid, the script/executable must: |
| // - Be inside the ksDir and named "generate" |
| // - Be executable (by somebody) |
| // - Accept a single argument indicating where the resulting ISO image should be saved (not |
| // verified by this function) |
| func customGenISOPath(dir string) string { |
| customPath := filepath.Join(dir, ksAltCommand) |
| stat, err := os.Stat(customPath) |
| |
| // Check if file exists and is executable. |
| const executablePermBits = 0111 |
| if err == nil && stat.Mode().Perm()&executablePermBits != 0 { |
| return customPath |
| } |
| return "" |
| } |
| |
| // kickstarterDir returns the directory containing the kickstarter files for |
| // the given OS. This is the directory passed to the mkisofs command. |
| // The root part of the directory can be overriden with a Parameter database entry. |
| func kickstarterDir(tx *sqlx.Tx, osVersionDir string) (string, error) { |
| var baseDir string |
| err := tx.QueryRow( |
| `SELECT value FROM parameter WHERE name = $1 AND config_file = $2 LIMIT 1`, |
| ksFilesParamName, |
| ksFilesParamConfigFile, |
| ).Scan(&baseDir) |
| if err != nil && err != sql.ErrNoRows { |
| return "", err |
| } |
| |
| if baseDir == "" { |
| baseDir = cfgDefaultDir |
| } |
| |
| return filepath.Join(baseDir, osVersionDir), nil |
| } |
| |
| // writeKSCfgs writes to the given directory the various Kickstart |
| // configuration files using the data from the given isoRequest. |
| // The cmd string is used to log to a file the command that will |
| // be executed to create the ISO. |
| func writeKSCfgs(dir string, r isoRequest, cmd string) error { |
| nameservers, err := readDefaultUnixResolve() |
| if err != nil { |
| return err |
| } |
| |
| // Create state.out |
| |
| stateFd, err := os.Create(filepath.Join(dir, ksStateOut)) |
| if err != nil { |
| return err |
| } |
| if _, err = fmt.Fprintf(stateFd, "Dir== %s\n%s\n", dir, cmd); err != nil { |
| return err |
| } |
| defer stateFd.Close() |
| |
| // Create network.cfg |
| |
| networkCfgFd, err := os.Create(filepath.Join(dir, ksCfgNetwork)) |
| if err != nil { |
| return err |
| } |
| defer networkCfgFd.Close() |
| if err = writeNetworkCfg(networkCfgFd, r, nameservers); err != nil { |
| return err |
| } |
| |
| // Create mgmt_network.cfg |
| |
| mgmtNetworkCfgFd, err := os.Create(filepath.Join(dir, ksCfgMgmtNetwork)) |
| if err != nil { |
| return err |
| } |
| defer mgmtNetworkCfgFd.Close() |
| if err = writeMgmtNetworkCfg(mgmtNetworkCfgFd, r); err != nil { |
| return err |
| } |
| |
| // Create password.cfg |
| |
| passwordCfgFd, err := os.Create(filepath.Join(dir, ksCfgPassword)) |
| if err != nil { |
| return err |
| } |
| defer passwordCfgFd.Close() |
| // Empty salt parameter causes a random salt to be generated, |
| // which is the desired behavior. |
| if err = writePasswordCfg(passwordCfgFd, r, ""); err != nil { |
| return err |
| } |
| |
| // Create disk.cfg |
| |
| diskCfgFd, err := os.Create(filepath.Join(dir, ksCfgDisk)) |
| if err != nil { |
| return err |
| } |
| defer diskCfgFd.Close() |
| if err = writeDiskCfg(diskCfgFd, r); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // bondedRegex matches a bonded device interface name. |
| var bondedRegex = regexp.MustCompile(`^bond\d+`) |
| |
| // writeNetworkCfg writes the network.cfg config to w. |
| func writeNetworkCfg(w io.Writer, r isoRequest, nameservers []string) error { |
| var cfg configWriter |
| |
| cfg.addIP("IPADDR", r.IPAddr) |
| cfg.addIP("NETMASK", r.IPNetmask) |
| cfg.addIP("GATEWAY", r.IPGateway) |
| isBonded := bondedRegex.MatchString(r.InterfaceName) |
| if isBonded { |
| cfg.addOpt("BOND_DEVICE", r.InterfaceName) |
| } else { |
| cfg.addOpt("DEVICE", r.InterfaceName) |
| } |
| cfg.addOpt("MTU", r.InterfaceMTU.String()) |
| cfg.addOpt("NAMESERVER", strings.Join(nameservers, ",")) |
| cfg.addOpt("HOSTNAME", r.fqdn()) |
| cfg.addOpt("NETWORKING_IPV6", "yes") |
| cfg.addOpt("IPV6ADDR", r.IP6Address) |
| cfg.addIP("IPV6_DEFAULTGW", r.IP6Gateway) |
| if isBonded { |
| cfg.addOpt("BONDING_OPTS", "miimon=100 mode=4 lacp_rate=fast xmit_hash_policy=layer3+4") |
| } |
| cfg.addBoolStr("DHCP", r.DHCP) |
| |
| _, err := io.Copy(w, &cfg) |
| return err |
| } |
| |
| // writeMgmtNetworkCfg writes the mgmt_network.cfg config to w. |
| func writeMgmtNetworkCfg(w io.Writer, r isoRequest) error { |
| var cfg configWriter |
| |
| // Test if management IP is IPv6 |
| if r.MgmtIPAddress.To16() != nil && r.MgmtIPAddress.To4() == nil { |
| cfg.addIP("IPV6ADDR", r.MgmtIPAddress) |
| } else { |
| cfg.addIP("IPADDR", r.MgmtIPAddress) |
| } |
| cfg.addIP("NETMASK", r.MgmtIPNetmask) |
| cfg.addIP("GATEWAY", r.MgmtIPGateway) |
| cfg.addOpt("DEVICE", r.MgmtInterface) |
| |
| _, err := io.Copy(w, &cfg) |
| return err |
| } |
| |
| // writeDiskCfg writes the disk.cfg config to w. |
| func writeDiskCfg(w io.Writer, r isoRequest) error { |
| var cfg configWriter |
| |
| cfg.addOpt("boot_drives", r.Disk) |
| |
| _, err := io.Copy(w, &cfg) |
| return err |
| } |
| |
| // writePasswordCfg writes the password.cfg config to w. |
| // The salt parameter is optional. If salt is blank, then a |
| // random 8-character salt will be used. |
| func writePasswordCfg(w io.Writer, r isoRequest, salt string) error { |
| if salt == "" { |
| salt = rndSalt(8) |
| } |
| |
| cryptedPw, err := crypt(r.RootPass, salt) |
| if err != nil { |
| return err |
| } |
| |
| _, err = fmt.Fprintf(w, "rootpw --iscrypted %s\n", cryptedPw) |
| return err |
| } |
| |
| // configWriter is a helper type to create config files |
| // of format: |
| // OPT="VALUE" |
| // OPT="VALUE" |
| type configWriter struct { |
| b bytes.Buffer |
| line int |
| } |
| |
| // addOpt adds an option to the config. |
| func (c *configWriter) addOpt(name, value string) { |
| if c.line > 0 { |
| fmt.Fprintln(&c.b) |
| } |
| c.line++ |
| fmt.Fprintf(&c.b, "%s=%q", name, value) |
| } |
| |
| // addIP adds an IPv4 option to the config. It handles the |
| // case where the given IP is empty/nil. |
| func (c *configWriter) addIP(name string, ip net.IP) { |
| // Avoid using `<nil>`, i.e. net.IP{}.String() = <nil> |
| var v string |
| if len(ip) > 0 { |
| v = ip.String() |
| } |
| c.addOpt(name, v) |
| } |
| |
| // addBoolStr adds a BoolStr option to the config, using |
| // "yes" / "no" values. |
| func (c *configWriter) addBoolStr(name string, b boolStr) { |
| var v string |
| if bv, _ := b.val(); bv { |
| v = "yes" |
| } else { |
| v = "no" |
| } |
| c.addOpt(name, v) |
| } |
| |
| // Read satisfies the io.Reader interface, and allows for |
| // using the configWriter by the io.Copy() function. |
| func (c *configWriter) Read(p []byte) (int, error) { |
| return c.b.Read(p) |
| } |