mirror of https://github.com/cbednarski/hostess
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
6.7 KiB
Go
244 lines
6.7 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/cbednarski/hostess/hostess"
|
|
)
|
|
|
|
const (
|
|
IPv4 = 1 << iota
|
|
IPv6 = 1 << iota
|
|
)
|
|
|
|
type Options struct {
|
|
IPVersion int
|
|
Preview bool
|
|
Force bool
|
|
}
|
|
|
|
// ErrCantWriteHostFile indicates that we are unable to write to the hosts file
|
|
var ErrCantWriteHostFile = ""
|
|
|
|
// ErrorLn will print an error message unless -s is passed
|
|
func ErrorLn(message string) {
|
|
os.Stderr.WriteString(fmt.Sprintf("%s\n", message))
|
|
}
|
|
|
|
// MaybePrintln will print a message unless -q or -s is passed
|
|
func MaybePrintln(options *Options, message string) {
|
|
if !AnyBool(c, "q") && !AnyBool(c, "s") {
|
|
fmt.Println(message)
|
|
}
|
|
}
|
|
|
|
// LoadHostfile will try to load, parse, and return a Hostfile. If we
|
|
// encounter errors we will terminate, unless -f is passed.
|
|
func LoadHostfile(options *Options) (*hostess.Hostfile, error) {
|
|
hosts, errs := hostess.LoadHostfile()
|
|
|
|
if len(errs) > 0 && !options.Force {
|
|
for _, err := range errs {
|
|
ErrorLn(err.Error())
|
|
}
|
|
return nil, errors.New("Errors while parsing hostsfile. Try hostess fmt")
|
|
}
|
|
|
|
return hosts, nil
|
|
}
|
|
|
|
// SaveOrPreview will display or write the Hostfile
|
|
func SaveOrPreview(options *Options, hostfile *hostess.Hostfile) error {
|
|
// If -n is passed, no-op and output the resultant hosts file to stdout.
|
|
// Otherwise it's for real and we're going to write it.
|
|
if options.Preview {
|
|
fmt.Printf("%s", hostfile.Format())
|
|
} else {
|
|
if err := hostfile.Save(); err != nil {
|
|
return fmt.Errorf("Unable to write to %s. Maybe you need to sudo? (error: %s)", hostess.GetHostsPath(), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StrPadRight adds spaces to the right of a string until it reaches length.
|
|
// If the input string is already that long, do nothing.
|
|
func StrPadRight(input string, length int) string {
|
|
minimum := len(input)
|
|
if length <= minimum {
|
|
return input
|
|
}
|
|
return input + strings.Repeat(" ", length-minimum)
|
|
}
|
|
|
|
// Add command parses <hostname> <ip> and adds or updates a hostname in the
|
|
// hosts file
|
|
func Add(options *Options, hostname, ip string) error {
|
|
hostsfile, err := LoadHostfile(options)
|
|
|
|
newHostname, err := hostess.NewHostname(hostname, ip, true)
|
|
if err != nil {
|
|
MaybeError(c, fmt.Sprintf("Failed to parse hosts entry: %s", err))
|
|
}
|
|
// If the command is aff instead of add then the entry should be disabled
|
|
if c.Command.Name == "aff" {
|
|
newHostname.Enabled = false
|
|
}
|
|
|
|
replace := hostsfile.Hosts.ContainsDomain(newHostname.Domain)
|
|
// Note that Add() may return an error, but they are informational only. We
|
|
// don't actually care what the error is -- we just want to add the
|
|
// hostname and save the file. This way the behavior is idempotent.
|
|
hostsfile.Hosts.Add(newHostname)
|
|
|
|
// If the user passes -n then we'll Add and show the new hosts file, but
|
|
// not save it.
|
|
if c.Bool("n") || AnyBool(c, "n") {
|
|
fmt.Printf("%s", hostsfile.Format())
|
|
} else {
|
|
SaveOrPreview(c, hostsfile)
|
|
// We'll give a little bit of information about whether we added or
|
|
// updated, but if the user wants to know they can use has or ls to
|
|
// show the file before they run the operation. Maybe later we can add
|
|
// a verbose flag to show more information.
|
|
if replace {
|
|
fmt.Printf("Updated %s\n", newHostname.FormatHuman())
|
|
} else {
|
|
fmt.Printf("Added %s\n", newHostname.FormatHuman())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove command removes any hostname(s) matching <domain> from the hosts file
|
|
func Remove(options *Options, hostname string) error {
|
|
hostsfile := LoadHostfile(c)
|
|
|
|
found := hostsfile.Hosts.ContainsDomain(hostname)
|
|
if found {
|
|
hostsfile.Hosts.RemoveDomain(hostname)
|
|
if AnyBool(c, "n") {
|
|
fmt.Printf("%s", hostsfile.Format())
|
|
} else {
|
|
SaveOrPreview(c, hostsfile)
|
|
MaybePrintln(c, fmt.Sprintf("Deleted %s", hostname))
|
|
}
|
|
} else {
|
|
MaybePrintln(c, fmt.Sprintf("%s not found in %s", hostname, hostess.GetHostsPath()))
|
|
}
|
|
}
|
|
|
|
// Has command indicates whether a hostname is present in the hosts file
|
|
func Has(options *Options, hostname string) error {
|
|
if len(c.Args()) != 1 {
|
|
MaybeError(c, "expected <hostname>")
|
|
}
|
|
domain := c.Args()[0]
|
|
hostsfile := LoadHostfile(c)
|
|
|
|
found := hostsfile.Hosts.ContainsDomain(domain)
|
|
if found {
|
|
MaybePrintln(c, fmt.Sprintf("Found %s in %s", domain, hostess.GetHostsPath()))
|
|
} else {
|
|
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, hostess.GetHostsPath()))
|
|
}
|
|
}
|
|
|
|
// OnOff enables (uncomments) or disables (comments) the specified hostname in
|
|
// the hosts file. Exits code 1 if the hostname is missing.
|
|
func OnOff(options *Options, hostname string) error {
|
|
hostsfile := LoadHostfile(c)
|
|
|
|
// Switch on / off commands
|
|
success := false
|
|
if c.Command.Name == "on" {
|
|
success = hostsfile.Hosts.Enable(domain)
|
|
} else {
|
|
success = hostsfile.Hosts.Disable(domain)
|
|
}
|
|
|
|
if success {
|
|
SaveOrPreview(c, hostsfile)
|
|
if c.Command.Name == "on" {
|
|
MaybePrintln(c, fmt.Sprintf("Enabled %s", domain))
|
|
} else {
|
|
MaybePrintln(c, fmt.Sprintf("Disabled %s", domain))
|
|
}
|
|
} else {
|
|
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, hostess.GetHostsPath()))
|
|
}
|
|
}
|
|
|
|
func Enable(options *Options, hostname string) error {
|
|
|
|
}
|
|
|
|
func Disable(options *Options, hostname string) error {
|
|
|
|
}
|
|
|
|
// List command shows a list of hostnames in the hosts file
|
|
func List(options *Options) error {
|
|
hostsfile := AlwaysLoadHostFile(c)
|
|
widestHostname := 0
|
|
widestIP := 0
|
|
for _, hostname := range hostsfile.Hosts {
|
|
dlen := len(hostname.Domain)
|
|
if dlen > widestHostname {
|
|
widestHostname = dlen
|
|
}
|
|
ilen := len(hostname.IP)
|
|
if ilen > widestIP {
|
|
widestIP = ilen
|
|
}
|
|
}
|
|
|
|
for _, hostname := range hostsfile.Hosts {
|
|
fmt.Printf("%s -> %s %s\n",
|
|
StrPadRight(hostname.Domain, widestHostname),
|
|
StrPadRight(hostname.IP.String(), widestIP),
|
|
hostname.FormatEnabled())
|
|
}
|
|
}
|
|
|
|
// Format command removes duplicates and conflicts from the hosts file
|
|
func Format(options *Options) error {
|
|
hostsfile := AlwaysLoadHostFile(c)
|
|
if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) {
|
|
MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts; nothing to do", hostess.GetHostsPath()))
|
|
os.Exit(0)
|
|
}
|
|
SaveOrPreview(c, hostsfile)
|
|
}
|
|
|
|
// Dump command outputs hosts file contents as JSON
|
|
func Dump(options *Options) error {
|
|
hostsfile := AlwaysLoadHostFile(c)
|
|
jsonbytes, err := hostsfile.Hosts.Dump()
|
|
if err != nil {
|
|
MaybeError(c, err.Error())
|
|
}
|
|
fmt.Println(fmt.Sprintf("%s", jsonbytes))
|
|
}
|
|
|
|
// Apply command adds hostnames to the hosts file from JSON
|
|
func Apply(options *Options, filename string) error {
|
|
jsonbytes, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
MaybeError(c, fmt.Sprintf("Unable to read %s: %s", filename, err))
|
|
}
|
|
|
|
hostfile := AlwaysLoadHostFile(c)
|
|
err = hostfile.Hosts.Apply(jsonbytes)
|
|
if err != nil {
|
|
MaybeError(c, fmt.Sprintf("Error applying changes to hosts file: %s", err))
|
|
}
|
|
|
|
SaveOrPreview(c, hostfile)
|
|
MaybePrintln(c, fmt.Sprintf("%s applied", filename))
|
|
}
|