├── boxen ├── assets │ ├── configs │ │ ├── ipinfusion_ocnos.template │ │ ├── checkpoint_cloudguard.template │ │ ├── cisco_xrv9k.template │ │ ├── paloalto_panos.template │ │ ├── cisco_n9kv.template │ │ ├── cisco_csr1000v.template │ │ ├── juniper_vsrx.template │ │ └── arista_veos.template │ ├── packaging │ │ ├── dockerignore.template │ │ ├── tc-tap-ifup │ │ ├── build.Dockerfile.template │ │ └── Dockerfile.template │ └── profiles │ │ ├── arista_veos.yaml │ │ ├── juniper_vsrx.yaml │ │ ├── cisco_csr1000v.yaml │ │ ├── cisco_xrv9k.yaml │ │ ├── cisco_n9kv.yaml │ │ ├── ipinfusion_ocnos.yaml │ │ ├── paloalto_panos.yaml │ │ └── checkpoint_cloudguard.yaml ├── assets.go ├── docker │ ├── util.go │ ├── wait.go │ ├── cp.go │ ├── build.go │ ├── run.go │ ├── common.go │ ├── commit.go │ ├── rm.go │ └── options.go ├── util │ ├── constants.go │ ├── network.go │ ├── env.go │ ├── timeouts.go │ ├── bytes.go │ ├── ints.go │ ├── strings.go │ ├── qemu.go │ ├── lockingwriter.go │ ├── errors.go │ ├── files.go │ ├── spinner.go │ └── md5crypt.go ├── logging │ ├── queue.go │ ├── socket.go │ ├── options.go │ ├── fifo.go │ ├── manager.go │ ├── writer.go │ ├── socketsender.go │ ├── message.go │ ├── socketreceiver.go │ └── instance.go ├── instance │ ├── base.go │ ├── constants.go │ ├── qemuoptions.go │ ├── logging.go │ └── qemuhealth.go ├── boxen │ ├── util.go │ ├── options.go │ ├── stop.go │ ├── deprovision.go │ ├── mgmtnet.go │ ├── defaults.go │ ├── configs.go │ ├── init.go │ ├── uninstall.go │ ├── mgmtnet_test.go │ ├── boxen.go │ ├── packageinstall.go │ ├── allocate.go │ ├── common.go │ ├── provision.go │ ├── packagestart.go │ └── start.go ├── config │ ├── interfaces.go │ ├── instance.go │ ├── global.go │ ├── platform.go │ ├── hardware.go │ ├── allocate.go │ └── config.go ├── cli │ ├── init.go │ ├── stop.go │ ├── deprovision.go │ ├── start.go │ ├── common.go │ ├── install.go │ ├── spin.go │ ├── packageinstall.go │ ├── uninstall.go │ ├── packagebuild.go │ ├── packagestart.go │ ├── provision.go │ └── cli.go ├── command │ ├── execute.go │ ├── options.go │ ├── sudo.go │ └── result.go └── platforms │ ├── common.go │ ├── timeouts.go │ ├── constants.go │ ├── options.go │ ├── util.go │ ├── base.go │ ├── checkpoint_cloudguard.go │ ├── ipinfusion_ocnos.go │ ├── factory.go │ ├── junipervsrx.go │ ├── ciscocsr1000v.go │ └── aristaveos.go ├── .goreleaser.yaml ├── .github ├── dependabot.yml └── workflows │ ├── publish.yaml │ └── commit.yaml ├── main.go ├── .gitignore ├── go.mod ├── LICENSE ├── Makefile ├── .golangci.yaml └── go.sum /boxen/assets/configs/ipinfusion_ocnos.template: -------------------------------------------------------------------------------- 1 | interface eth0 2 | ip address 10.0.0.15/24 -------------------------------------------------------------------------------- /boxen/assets.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import "embed" 4 | 5 | //go:embed assets/* 6 | var Assets embed.FS 7 | -------------------------------------------------------------------------------- /boxen/assets/packaging/dockerignore.template: -------------------------------------------------------------------------------- 1 | {{range $index, $file := .RequiredFiles -}} 2 | {{$file}} 3 | {{end}} -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | project_name: boxen 3 | builds: 4 | - id: boxen-bin 5 | ldflags: 6 | - -s -w -X github.com/carlmontanari/boxen/boxen/boxen.Version={{.Version}} 7 | -------------------------------------------------------------------------------- /boxen/assets/configs/checkpoint_cloudguard.template: -------------------------------------------------------------------------------- 1 | lock database override 2 | set interface eth0 ipv4-address 10.0.0.15 subnet-mask 255.255.255.0 3 | set interface eth0 state on 4 | set ipv6-state on 5 | unlock database -------------------------------------------------------------------------------- /boxen/assets/configs/cisco_xrv9k.template: -------------------------------------------------------------------------------- 1 | interface MgmtEth 0/RP0/CPU0/0 2 | ip address 10.0.0.15/24 3 | no shutdown 4 | exit 5 | ssh server v2 6 | ssh server netconf vrf default 7 | netconf-yang agent ssh 8 | grpc port 57400 9 | grpc no-tls -------------------------------------------------------------------------------- /boxen/assets/configs/paloalto_panos.template: -------------------------------------------------------------------------------- 1 | set deviceconfig system ip-address 10.0.0.15 netmask 255.255.255.0 default-gateway 10.0.0.2 2 | set mgt-config users {{ .Username }} permissions role-based superuser yes 3 | set mgt-config users {{ .Username }} phash {{ .Password }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "sunday" 9 | timezone: "PST8PDT" 10 | time: "03:00" 11 | target-branch: "develop" 12 | -------------------------------------------------------------------------------- /boxen/assets/configs/cisco_n9kv.template: -------------------------------------------------------------------------------- 1 | no password strength-check 2 | username {{ .Username }} password 0 {{ .Password }} role network-admin 3 | interface mgmt0 4 | ip address 10.0.0.15/24 5 | no shutdown 6 | exit 7 | feature scp-server 8 | feature nxapi 9 | feature netconf 10 | feature grpc -------------------------------------------------------------------------------- /boxen/docker/util.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | // ReadCidFile loads provided cidfile f and returns trimmed string contents. 9 | func ReadCidFile(f string) string { 10 | o, _ := os.ReadFile(f) 11 | return strings.TrimSpace(string(o)) 12 | } 13 | -------------------------------------------------------------------------------- /boxen/assets/profiles/arista_veos.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 4096 4 | acceleration: 5 | - kvm 6 | - hax 7 | serial_port_count: 1 8 | nic_type: e1000 9 | nic_count: 20 10 | nic_per_bus: 26 11 | advanced: {} 12 | tcp_nat_ports: 13 | - 22 14 | - 23 15 | - 443 16 | - 830 17 | udp_nat_ports: 18 | - 161 -------------------------------------------------------------------------------- /boxen/assets/profiles/juniper_vsrx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 8192 4 | acceleration: 5 | - kvm 6 | - hax 7 | serial_port_count: 1 8 | nic_type: virtio-net-pci 9 | nic_count: 20 10 | nic_per_bus: 26 11 | advanced: {} 12 | tcp_nat_ports: 13 | - 22 14 | - 23 15 | - 443 16 | - 830 17 | udp_nat_ports: 18 | - 161 -------------------------------------------------------------------------------- /boxen/assets/profiles/cisco_csr1000v.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 4096 4 | acceleration: 5 | - kvm 6 | - hvf 7 | - hax 8 | serial_port_count: 1 9 | nic_type: virtio-net-pci 10 | nic_count: 9 11 | nic_per_bus: 26 12 | advanced: {} 13 | tcp_nat_ports: 14 | - 22 15 | - 23 16 | - 443 17 | - 830 18 | udp_nat_ports: 19 | - 161 -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/carlmontanari/boxen/boxen/logging" 8 | 9 | "github.com/carlmontanari/boxen/boxen/cli" 10 | ) 11 | 12 | func main() { 13 | err := cli.NewCLI().Run(os.Args) 14 | 15 | logging.Manager.Terminate() 16 | 17 | if err != nil { 18 | fmt.Println(err) 19 | 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /boxen/util/constants.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | const ( 4 | MaxBuffer = 65535 5 | FilePerms = 0666 6 | QemuImgCmd = "qemu-img" 7 | DockerCmd = "docker" 8 | QemuImgContainer = "ghcr.io/hellt/qemu-img:latest" 9 | ISOBinary = "genisoimage" 10 | DarwinISOBinary = "mkisofs" 11 | ISOBinaryContainer = "ghcr.io/hellt/cdrkit:1.11.11-r3" 12 | ) 13 | -------------------------------------------------------------------------------- /boxen/assets/profiles/cisco_xrv9k.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 16384 4 | acceleration: 5 | - kvm 6 | - hvf 7 | serial_port_count: 1 8 | nic_type: virtio-net-pci 9 | nic_count: 16 10 | nic_per_bus: 26 11 | advanced: 12 | cpu: 13 | emulation: "qemu64,+ssse3,+sse4.1,+sse4.2" 14 | tcp_nat_ports: 15 | - 22 16 | - 23 17 | - 443 18 | - 830 19 | udp_nat_ports: 20 | - 161 -------------------------------------------------------------------------------- /boxen/logging/queue.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "sync" 4 | 5 | // Queue represents a single log message queue. 6 | type Queue struct { 7 | queue []*Message 8 | depth int 9 | lock *sync.Mutex 10 | } 11 | 12 | // newInstanceQueue returns a new Queue with nothing but the lock attribute instantiated. 13 | func newInstanceQueue() *Queue { 14 | return &Queue{ 15 | lock: &sync.Mutex{}, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /boxen/assets/configs/cisco_csr1000v.template: -------------------------------------------------------------------------------- 1 | username {{ .Username }} privilege 15 password {{ .Password }} 2 | enable secret 0 {{ .Password }} 3 | interface GigabitEthernet1 4 | ip address 10.0.0.15 255.255.255.0 5 | no shutdown 6 | exit 7 | ip domain name boxen.box 8 | hostname router 9 | crypto key generate rsa modulus 2048 10 | restconf 11 | netconf-yang 12 | line vty 0 4 13 | login local 14 | transport input all -------------------------------------------------------------------------------- /boxen/assets/profiles/cisco_n9kv.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 8192 4 | acceleration: 5 | - kvm 6 | - none 7 | serial_port_count: 1 8 | nic_type: e1000 9 | nic_count: 8 10 | nic_per_bus: 26 11 | advanced: 12 | cpu: 13 | emulation: max 14 | cores: 8 15 | threads: 1 16 | sockets: 1 17 | tcp_nat_ports: 18 | - 22 19 | - 23 20 | - 443 21 | - 830 22 | udp_nat_ports: 23 | - 161 -------------------------------------------------------------------------------- /boxen/instance/base.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | // Option is the option function used to apply instance options. 4 | type Option func(interface{}) error 5 | 6 | // Base is the bare minimum interface a "Platform" must implement in order for boxen to manage an 7 | // "instance". 8 | type Base interface { 9 | Install(...Option) error 10 | Start(...Option) error 11 | Stop(...Option) error 12 | RunUntilSigInt() 13 | } 14 | -------------------------------------------------------------------------------- /boxen/logging/socket.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | const ( 8 | tcp = "tcp" 9 | ) 10 | 11 | var ( 12 | ErrSocketFailure = errors.New("error creating socket connection") 13 | ) 14 | 15 | // ConnData is a struct representing common data attributes required for socket sender and receiver. 16 | type ConnData struct { 17 | Protocol string 18 | Address string 19 | Port int 20 | } 21 | -------------------------------------------------------------------------------- /boxen/assets/profiles/ipinfusion_ocnos.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 4096 4 | acceleration: 5 | - kvm 6 | - hax 7 | serial_port_count: 1 8 | nic_type: virtio-net-pci 9 | nic_count: 6 10 | nic_per_bus: 26 11 | advanced: 12 | cpu: 13 | emulation: host 14 | cores: 2 15 | threads: 1 16 | sockets: 1 17 | tcp_nat_ports: 18 | - 22 19 | - 23 20 | - 443 21 | - 830 22 | udp_nat_ports: 23 | - 161 24 | -------------------------------------------------------------------------------- /boxen/util/network.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | // GetPreferredIP returns the IP address the host machine prefers for outbound requests. 9 | func GetPreferredIP() string { 10 | conn, err := net.Dial("udp", "8.8.8.8:80") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer conn.Close() 15 | 16 | localAddr := conn.LocalAddr().(*net.UDPAddr) 17 | 18 | return localAddr.IP.String() 19 | } 20 | -------------------------------------------------------------------------------- /boxen/assets/profiles/paloalto_panos.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 8192 4 | acceleration: 5 | - kvm 6 | - hvf 7 | - hax 8 | serial_port_count: 1 9 | nic_type: virtio-net-pci 10 | nic_count: 9 11 | nic_per_bus: 26 12 | advanced: 13 | cpu: 14 | emulation: host 15 | cores: 1 16 | threads: 1 17 | sockets: 2 18 | tcp_nat_ports: 19 | - 22 20 | - 23 21 | - 443 22 | - 830 23 | udp_nat_ports: 24 | - 161 -------------------------------------------------------------------------------- /boxen/assets/configs/juniper_vsrx.template: -------------------------------------------------------------------------------- 1 | set interfaces fxp0 unit 0 family inet address 10.0.0.15/24 2 | delete interfaces fxp0 unit 0 family inet dhcp 3 | delete system processes dhcp-service 4 | set system services ssh 5 | set system services netconf ssh 6 | set system services netconf rfc-compliant 7 | set system root-authentication encrypted-password {{ .Password }} 8 | set system login user {{ .Username }} class super-user authentication encrypted-password {{ .Password }} -------------------------------------------------------------------------------- /boxen/util/env.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | func GetEnvIntOrDefault(k string, d int) int { 9 | if v, ok := os.LookupEnv(k); ok { 10 | ev, err := strconv.Atoi(v) 11 | 12 | if err != nil { 13 | return d 14 | } 15 | 16 | return ev 17 | } 18 | 19 | return d 20 | } 21 | 22 | func GetEnvStrOrDefault(k, d string) string { 23 | if v, ok := os.LookupEnv(k); ok { 24 | return v 25 | } 26 | 27 | return d 28 | } 29 | -------------------------------------------------------------------------------- /boxen/assets/configs/arista_veos.template: -------------------------------------------------------------------------------- 1 | username {{ .Username }} secret 0 {{ .Password }} role network-admin{{ if ne .Password "" }} 2 | enable secret 0 {{ .Password }}{{ end }} 3 | interface Management 1 4 | ip address 10.0.0.15 255.255.255.0 5 | no shutdown 6 | exit 7 | management api http-commands 8 | protocol unix-socket 9 | no shutdown 10 | exit 11 | management api gnmi 12 | transport grpc default 13 | no shutdown 14 | exit 15 | management api netconf 16 | transport ssh default 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # goland 16 | .idea 17 | 18 | # macos stuff 19 | .DS_Store 20 | */.DS_Store 21 | 22 | # private dir for notes and such 23 | private/ 24 | 25 | # log output 26 | *.log 27 | 28 | # mkdocs site 29 | site/* 30 | 31 | # vscode 32 | .vscode 33 | .devcontainer -------------------------------------------------------------------------------- /boxen/boxen/util.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/util" 7 | ) 8 | 9 | // GetGroupInstances returns a slice of instance names for a provided group. 10 | func (b *Boxen) GetGroupInstances(group string) ([]string, error) { 11 | instances, ok := b.Config.InstanceGroups[group] 12 | if !ok { 13 | return nil, fmt.Errorf( 14 | "%w: unknown group name '%s' provided", 15 | util.ErrAllocationError, 16 | group, 17 | ) 18 | } 19 | 20 | return instances, nil 21 | } 22 | -------------------------------------------------------------------------------- /boxen/assets/profiles/checkpoint_cloudguard.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hardware: 3 | memory: 8192 4 | acceleration: 5 | - kvm 6 | serial_port_count: 1 7 | nic_type: virtio-net-pci 8 | nic_count: 8 9 | nic_per_bus: 26 10 | advanced: 11 | cpu: 12 | emulation: host 13 | cores: 4 14 | tcp_nat_ports: 15 | - 22 16 | - 23 17 | - 257 18 | - 443 19 | - 830 20 | - 4434 21 | - 8211 22 | - 18190 23 | - 18191 24 | - 18192 25 | - 18210 26 | - 18211 27 | - 18221 28 | - 18264 29 | - 19009 30 | udp_nat_ports: 31 | - 161 32 | -------------------------------------------------------------------------------- /boxen/util/timeouts.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // GetTimeoutMultiplier returns either 1, or the integer value of the environment variable 4 | // BOXEN_TIMEOUT_MULTIPLIER. 5 | func GetTimeoutMultiplier() int { 6 | return GetEnvIntOrDefault( 7 | "BOXEN_TIMEOUT_MULTIPLIER", 8 | 1, 9 | ) 10 | } 11 | 12 | // ApplyTimeoutMultiplier returns the timeout, as an integer, after being multiplied by the 13 | // environment variable BOXEN_TIMEOUT_MULTIPLIER. 14 | func ApplyTimeoutMultiplier(timeout int) int { 15 | return timeout * GetTimeoutMultiplier() 16 | } 17 | -------------------------------------------------------------------------------- /boxen/util/bytes.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "bytes" 4 | 5 | // ByteSliceAllNull checks if a byte slice contains onlyl null bytes. 6 | func ByteSliceAllNull(b []byte) bool { 7 | for _, v := range b { 8 | if v != 0 { 9 | return false 10 | } 11 | } 12 | 13 | return true 14 | } 15 | 16 | // ByteSliceContains checks if slice of bytes contains a given byte subslice. 17 | func ByteSliceContains(b [][]byte, l []byte) bool { 18 | for _, bs := range b { 19 | if bytes.Contains(l, bs) { 20 | return true 21 | } 22 | } 23 | 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /boxen/logging/options.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Option func(i *Instance) error 9 | 10 | func WithLevel(l string) Option { 11 | return func(i *Instance) error { 12 | l = strings.ToLower(l) 13 | 14 | for _, v := range []string{debug, info, critical} { 15 | if l == v { 16 | i.level = levelMap[l] 17 | 18 | return nil 19 | } 20 | } 21 | 22 | return fmt.Errorf( 23 | "%w: invalid logging level '%s' provided, must be one of 'debug', 'info', 'critical'", 24 | ErrLogError, l, 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /boxen/boxen/options.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import "github.com/carlmontanari/boxen/boxen/logging" 4 | 5 | type Option func(*args) error 6 | 7 | // WithConfig allows for passing a config file path f to the NewBoxen function. 8 | func WithConfig(f string) Option { 9 | return func(o *args) error { 10 | o.config = f 11 | 12 | return nil 13 | } 14 | } 15 | 16 | // WithLogger allows for passing a boxen logging.Instance l to the NewBoxen function. 17 | func WithLogger(l *logging.Instance) Option { 18 | return func(o *args) error { 19 | o.logger = l 20 | 21 | return nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.18 19 | - uses: goreleaser/goreleaser-action@v2 20 | with: 21 | distribution: goreleaser 22 | version: latest 23 | args: release --rm-dist 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /boxen/assets/packaging/tc-tap-ifup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TAP_IF=$1 3 | # get interface index number up to 3 digits (everything after first three chars) 4 | # tap0 -> 0 5 | # tap123 -> 123 6 | INDEX=${TAP_IF:3:3} 7 | ip link set $TAP_IF up 8 | ip link set $TAP_IF mtu 65000 9 | # create tc eth<->tap redirect rules 10 | tc qdisc add dev eth$INDEX ingress 11 | tc filter add dev eth$INDEX parent ffff: protocol all u32 match u8 0 0 action mirred egress redirect dev tap$INDEX 12 | tc qdisc add dev $TAP_IF ingress 13 | tc filter add dev $TAP_IF parent ffff: protocol all u32 match u8 0 0 action mirred egress redirect dev eth$INDEX -------------------------------------------------------------------------------- /boxen/util/ints.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // IntSliceContains is a convenience function to check if a provided int i is in an int slice s. 4 | func IntSliceContains(s []int, i int) bool { 5 | for _, ss := range s { 6 | if ss == i { 7 | return true 8 | } 9 | } 10 | 11 | return false 12 | } 13 | 14 | // IntSliceUniqify removes any duplicated entries in a slice of integers s. 15 | func IntSliceUniqify(s []int) []int { 16 | var unique []int 17 | 18 | keys := make(map[int]bool) 19 | 20 | for _, entry := range s { 21 | if _, value := keys[entry]; !value { 22 | keys[entry] = true 23 | 24 | unique = append(unique, entry) 25 | } 26 | } 27 | 28 | return unique 29 | } 30 | -------------------------------------------------------------------------------- /boxen/instance/constants.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | const ( 4 | // AccelKVM represents KVM acceleration. 5 | AccelKVM = "kvm" 6 | // AccelHVF represents HVF (Darwin) acceleration. 7 | AccelHVF = "hvf" 8 | // AccelHAX represents HAX (Intel Haxm) acceleration. 9 | AccelHAX = "hax" 10 | // AccelNone represents no acceleration. 11 | AccelNone = "none" 12 | // OptNone represents a literal string "none" to be used for some qemu command options. 13 | OptNone = "none" 14 | // OptMachinePc represents the Qemu "machine" type of "pc". 15 | OptMachinePc = "pc" 16 | // NicE1000 represents an E1000 virtual nic. 17 | NicE1000 = "e1000" 18 | // NicVirtio represents an virtio-net-pci virtual nic. 19 | NicVirtio = "virtio-net-pci" 20 | ) 21 | -------------------------------------------------------------------------------- /boxen/docker/wait.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/command" 5 | ) 6 | 7 | // Wait runs the docker "wait" command for a provided container ID (as provided in options). 8 | func Wait(opts ...Option) error { 9 | a := &args{} 10 | 11 | for _, o := range opts { 12 | err := o(a) 13 | 14 | if err != nil { 15 | return err 16 | } 17 | } 18 | 19 | if a.container == "" { 20 | panic("container id not provided, can't wait") 21 | } 22 | 23 | cmdArgs := []string{"wait", a.container} 24 | 25 | executeArgs := setExecuteArgs(a) 26 | 27 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 28 | 29 | r, err := command.Execute(dockerCmd, executeArgs...) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return r.Proc.Wait() 35 | } 36 | -------------------------------------------------------------------------------- /boxen/util/strings.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // StringSliceContains checks if slice of strings contains a given string. 4 | func StringSliceContains(s string, l []string) bool { 5 | for _, ss := range l { 6 | if ss == s { 7 | return true 8 | } 9 | } 10 | 11 | return false 12 | } 13 | 14 | // AnyStringVal returns true if any string in variadic s matches the string val. 15 | func AnyStringVal(val string, s ...string) bool { 16 | for _, v := range s { 17 | if v == val { 18 | return true 19 | } 20 | } 21 | 22 | return false 23 | } 24 | 25 | // AllStringVal returns true if all strings in variadic s matches the string val. 26 | func AllStringVal(val string, s ...string) bool { 27 | for _, v := range s { 28 | if v != val { 29 | return false 30 | } 31 | } 32 | 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Commit 3 | 4 | on: [push, pull_request, workflow_dispatch] 5 | 6 | jobs: 7 | unit-test: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | max-parallel: 2 11 | matrix: 12 | os: [ubuntu-latest, macos-latest] 13 | version: ["1.18"] 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v3 17 | - name: set up go ${{ matrix.version }} 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.version }} 21 | - name: lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.45 25 | - name: install gotestsum 26 | run: go install gotest.tools/gotestsum@latest 27 | - name: tests 28 | run: make test-race 29 | -------------------------------------------------------------------------------- /boxen/logging/fifo.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | type FifoLogQueue struct { 9 | q []string 10 | lock *sync.Mutex 11 | } 12 | 13 | func NewFifoLogQueue() *FifoLogQueue { 14 | return &FifoLogQueue{ 15 | q: nil, 16 | lock: &sync.Mutex{}, 17 | } 18 | } 19 | 20 | func (l *FifoLogQueue) Accept(o ...interface{}) { 21 | l.lock.Lock() 22 | defer l.lock.Unlock() 23 | 24 | if len(o) == 0 { 25 | return 26 | } 27 | 28 | s, ok := o[0].(string) 29 | if !ok { 30 | return 31 | } 32 | 33 | l.q = append(l.q, s) 34 | } 35 | 36 | func (l *FifoLogQueue) Emit() string { 37 | l.lock.Lock() 38 | defer l.lock.Unlock() 39 | 40 | var e []string 41 | 42 | for len(l.q) > 0 { 43 | e = append(e, l.q[0]) 44 | l.q = l.q[1:] 45 | } 46 | 47 | return strings.Join(e, "\n") 48 | } 49 | -------------------------------------------------------------------------------- /boxen/config/interfaces.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type MgmtIntf struct { 6 | Nat *Nat 7 | Bridge *Bridge 8 | } 9 | 10 | type Nat struct { 11 | TCP []*NatPortPair `yaml:"tcp,omitempty"` 12 | UDP []*NatPortPair `yaml:"udp,omitempty"` 13 | } 14 | 15 | type NatPortPair struct { 16 | InstanceSide int `yaml:"instance_side,omitempty"` 17 | HostSide int `yaml:"host_side,omitempty"` 18 | } 19 | 20 | func (n *NatPortPair) String() string { 21 | return fmt.Sprintf("(host)%d<->%d(instance)", n.HostSide, n.InstanceSide) 22 | } 23 | 24 | type Bridge struct{} 25 | 26 | type DataPlaneIntf struct { 27 | SocketConnectMap map[int]*SocketConnectPair `yaml:"socket_connect_map,omitempty"` 28 | } 29 | 30 | type SocketConnectPair struct { 31 | Connect int `yaml:"connect,omitempty"` 32 | Listen int `yaml:"listen,omitempty"` 33 | } 34 | -------------------------------------------------------------------------------- /boxen/instance/qemuoptions.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import "github.com/carlmontanari/boxen/boxen/util" 4 | 5 | type qemuOpts struct { 6 | launchModifier func(c *QemuLaunchCmd) 7 | sudo bool 8 | } 9 | 10 | // WithLaunchModifier sets an option to modify qemu launch command. 11 | func WithLaunchModifier(f func(c *QemuLaunchCmd)) Option { 12 | return func(o interface{}) error { 13 | q, ok := o.(*qemuOpts) 14 | 15 | if ok { 16 | q.launchModifier = f 17 | return nil 18 | } 19 | 20 | return util.ErrIgnoredOption 21 | } 22 | } 23 | 24 | // WithSudo tells boxen to start/stop instances with sudo or not. 25 | func WithSudo(b bool) Option { 26 | return func(o interface{}) error { 27 | q, ok := o.(*qemuOpts) 28 | 29 | if ok { 30 | q.sudo = b 31 | return nil 32 | } 33 | 34 | return util.ErrIgnoredOption 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /boxen/logging/manager.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | // Manager is a singleton/global log manager instance. 4 | var Manager = &manager{} //nolint:gochecknoglobals 5 | 6 | // manager is the instance that holds all loggers. 7 | type manager struct { 8 | loggers []*Instance 9 | } 10 | 11 | // AddInstance adds a logging instance *Instance to the log manager. 12 | func (l *manager) addInstance(li *Instance) { 13 | li.Start() 14 | 15 | l.loggers = append(l.loggers, li) 16 | } 17 | 18 | // Terminate terminates all the logging instances that the manager holds. 19 | func (l *manager) Terminate() { 20 | for _, li := range l.loggers { 21 | // iterate over all loggers and set the done flag, so we can terminate the program and all 22 | // logger goroutines 23 | li.setDone(true) 24 | } 25 | 26 | for _, li := range l.loggers { 27 | li.wg.Wait() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /boxen/cli/init.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/boxen" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func initCommands() []*cli.Command { 10 | directory := &cli.StringFlag{ 11 | Name: "directory", 12 | Usage: "directory to initialize boxen in", 13 | Required: false, 14 | Value: "~/boxen", 15 | } 16 | 17 | return []*cli.Command{ 18 | { 19 | Name: "init", 20 | Usage: "initialize a boxen config/directory structure", 21 | Flags: []cli.Flag{ 22 | directory, 23 | }, 24 | Action: func(c *cli.Context) error { 25 | return Init( 26 | c.String("directory"), 27 | ) 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func Init(directory string) error { 34 | b, err := boxen.NewBoxen() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | err = b.Init(directory) 40 | 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /boxen/docker/cp.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | // CopyFromContainer copies file/path 's' from the container ID provided in the options to the 10 | // destination 'd' on the local filesystem. 11 | func CopyFromContainer(s, d string, opts ...Option) error { 12 | a := &args{} 13 | 14 | for _, o := range opts { 15 | err := o(a) 16 | 17 | if err != nil { 18 | return err 19 | } 20 | } 21 | 22 | if a.container == "" { 23 | panic("container id not provided, can't copy") 24 | } 25 | 26 | cmdArgs := []string{"cp", fmt.Sprintf("%s:%s", a.container, s), d} 27 | 28 | executeArgs := setExecuteArgs(a) 29 | 30 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 31 | 32 | r, err := command.Execute(dockerCmd, executeArgs...) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return r.Proc.Wait() 38 | } 39 | -------------------------------------------------------------------------------- /boxen/boxen/stop.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/instance" 5 | "github.com/carlmontanari/boxen/boxen/platforms" 6 | ) 7 | 8 | // Stop stops a local boxen instance. 9 | func (b *Boxen) Stop(name string) error { 10 | b.Logger.Infof("stop for instance '%s' requested", name) 11 | 12 | q, err := platforms.NewPlatformFromConfig( 13 | name, 14 | b.Config, 15 | &instance.Loggers{ 16 | Base: b.Logger, 17 | Stdout: nil, 18 | Stderr: nil, 19 | Console: nil, 20 | }, 21 | ) 22 | if err != nil { 23 | b.Logger.Criticalf("error spawning instance from config: %s", err) 24 | 25 | return err 26 | } 27 | 28 | b.modifyInstanceMap(func() { b.Instances[name] = q }) 29 | 30 | err = q.Stop(instance.WithSudo(true)) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | b.Logger.Infof("stop for instance '%s' completed successfully", name) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /boxen/config/instance.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Instance is a struct that represents a qemu virtual machine instance in the boxen configuration. 4 | type Instance struct { 5 | Name string `yaml:"name"` 6 | PlatformType string `yaml:"platform_type"` 7 | Disk string `yaml:"source_disk"` 8 | ID int `yaml:"id,omitempty"` 9 | PID int `yaml:"pid,omitempty"` 10 | Profile string `yaml:"profile,omitempty"` 11 | Credentials *Credentials `yaml:"credentials,omitempty"` 12 | Hardware *Hardware `yaml:"hardware,omitempty"` 13 | MgmtIntf *MgmtIntf `yaml:"mgmt_interface,omitempty"` 14 | DataPlaneIntf *DataPlaneIntf `yaml:"data_plane_interfaces,omitempty"` 15 | Advanced *Advanced `yaml:"advanced,omitempty"` 16 | BootDelay int `yaml:"boot-delay,omitempty"` 17 | StartupConfig string `yaml:"startup-config,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /boxen/docker/build.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | func Build(opts ...Option) error { 10 | a := &args{} 11 | a.repo = "boxen" 12 | a.tag = "latest" 13 | 14 | for _, o := range opts { 15 | err := o(a) 16 | 17 | if err != nil { 18 | return err 19 | } 20 | } 21 | 22 | cmdArgs := []string{"build"} 23 | 24 | if a.workDir != "" { 25 | cmdArgs = append(cmdArgs, a.workDir) 26 | } 27 | 28 | if a.dockerfile != "" { 29 | cmdArgs = append(cmdArgs, "-f", a.dockerfile) 30 | } 31 | 32 | if a.nocache { 33 | cmdArgs = append(cmdArgs, "--no-cache") 34 | } 35 | 36 | cmdArgs = append(cmdArgs, "-t", fmt.Sprintf("%s:%s", a.repo, a.tag)) 37 | 38 | executeArgs := setExecuteArgs(a) 39 | 40 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 41 | 42 | r, err := command.Execute(dockerCmd, executeArgs...) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return r.Proc.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/carlmontanari/boxen 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/scrapli/scrapligo v1.1.0 8 | github.com/scrapli/scrapligocfg v1.0.0 9 | github.com/urfave/cli/v2 v2.3.0 10 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 11 | gopkg.in/yaml.v2 v2.4.0 12 | ) 13 | 14 | require ( 15 | github.com/carlmontanari/difflibgo v0.0.0-20210718194309-31b9e131c298 // indirect 16 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 17 | github.com/creack/pty v1.1.18 // indirect 18 | github.com/google/go-cmp v0.5.8 // indirect 19 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 20 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 21 | github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 // indirect 22 | golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect 23 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect 24 | gopkg.in/yaml.v3 v3.0.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /boxen/boxen/deprovision.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // DeProvision does what it says -- it "deprovisions" an instance from the Boxen configuration. This 9 | // is only useful/relevant for "local" boxen operations, i.e. VMs not containerized/packaged as you 10 | // would use with Containerlab. 11 | func (b *Boxen) DeProvision(instance string) error { 12 | b.Logger.Infof("de-provision for instance '%s' requested", instance) 13 | 14 | err := os.RemoveAll(fmt.Sprintf("%s/%s", b.Config.Options.Build.InstancePath, instance)) 15 | if err != nil { 16 | b.Logger.Criticalf("error deleting instance directory: %s", err) 17 | 18 | return err 19 | } 20 | 21 | b.Config.DeleteInstance(instance) 22 | 23 | err = b.Config.Dump(b.ConfigPath) 24 | if err != nil { 25 | b.Logger.Criticalf("error dumping updated boxen config to disk: %s", err) 26 | 27 | return err 28 | } 29 | 30 | b.Logger.Infof("de-provision for instance '%s' completed successfully", instance) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /boxen/docker/run.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | func Run(opts ...Option) (*command.Result, error) { 10 | a := &args{} 11 | 12 | for _, o := range opts { 13 | err := o(a) 14 | 15 | if err != nil { 16 | return nil, err 17 | } 18 | } 19 | 20 | if a.repo == "" || a.tag == "" { 21 | panic("repo and tag not provided, can't run container") 22 | } 23 | 24 | cmdArgs := []string{"run"} 25 | 26 | if a.cidFile != "" { 27 | cmdArgs = append(cmdArgs, "--cidfile", a.cidFile) 28 | } 29 | 30 | if a.privileged { 31 | cmdArgs = append(cmdArgs, "--privileged") 32 | } 33 | 34 | cmdArgs = append(cmdArgs, fmt.Sprintf("%s:%s", a.repo, a.tag)) 35 | 36 | executeArgs := setExecuteArgs(a) 37 | 38 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 39 | 40 | r, err := command.Execute(dockerCmd, executeArgs...) 41 | if err != nil { 42 | return r, err 43 | } 44 | 45 | err = r.CheckStdErr() 46 | 47 | return r, err 48 | } 49 | -------------------------------------------------------------------------------- /boxen/config/global.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type GlobalOptions struct { 4 | Credentials *Credentials `yaml:"credentials,omitempty"` 5 | Qemu *Qemu `yaml:"qemu,omitempty"` 6 | Build *Build `yaml:"build,omitempty"` 7 | } 8 | 9 | type Credentials struct { 10 | Username string `yaml:"username,omitempty"` 11 | Password string `yaml:"password,omitempty"` 12 | } 13 | 14 | // NewDefaultCredentials returns a Credentials object with the boxen default creds set. 15 | func NewDefaultCredentials() *Credentials { 16 | return &Credentials{ 17 | Username: "boxen", 18 | Password: "b0x3N-b0x3N", 19 | } 20 | } 21 | 22 | type Qemu struct { 23 | Acceleration []string `yaml:"acceleration,omitempty"` 24 | Binary string `yaml:"binary,omitempty"` 25 | // UseThickDisks copies full disks instead of creating qemu disks with a backing chain 26 | UseThickDisks bool `yaml:"use_thick_disks,omitempty"` 27 | } 28 | 29 | type Build struct { 30 | InstancePath string `yaml:"instance_path,omitempty"` 31 | SourcePath string `yaml:"source_path,omitempty"` 32 | } 33 | -------------------------------------------------------------------------------- /boxen/docker/common.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | const ( 10 | dockerCmd = "docker" 11 | ) 12 | 13 | type args struct { 14 | workDir string 15 | dockerfile string 16 | repo string 17 | tag string 18 | container string 19 | cidFile string 20 | privileged bool 21 | commitChange string 22 | nocache bool 23 | stdOut io.Writer 24 | stdErr io.Writer 25 | } 26 | 27 | func setExecuteArgs(a *args) []command.ExecuteOption { 28 | var executeArgs []command.ExecuteOption 29 | 30 | if a.workDir != "" { 31 | executeArgs = append(executeArgs, command.WithWorkDir(a.workDir)) 32 | } 33 | 34 | if a.stdOut != nil { 35 | executeArgs = append(executeArgs, command.WithStdOut(a.stdOut)) 36 | } 37 | 38 | if a.stdErr != nil { 39 | executeArgs = append(executeArgs, command.WithStdErr(a.stdErr)) 40 | } 41 | 42 | if a.privileged { 43 | executeArgs = append(executeArgs, command.WithSudo(true)) 44 | } 45 | 46 | return executeArgs 47 | } 48 | -------------------------------------------------------------------------------- /boxen/config/platform.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Profile is a struct containing information about the virtual machine hardware and port allocation 4 | // as stored in the boxen configuration. 5 | type Profile struct { 6 | Hardware *ProfileHardware `yaml:"hardware,omitempty"` 7 | Advanced *Advanced `yaml:"advanced,omitempty"` 8 | TPCNatPorts []int `yaml:"tcp_nat_ports,omitempty"` 9 | UDPNatPorts []int `yaml:"udp_nat_ports,omitempty"` 10 | } 11 | 12 | // Platform contains information about a given platform -- i.e. Arista vEOS -- that is stored in the 13 | // boxen configuration this includes the available hardware profiles and source disks that have 14 | // been "installed". 15 | type Platform struct { 16 | SourceDisks []string `yaml:"source_disks,omitempty"` 17 | Profiles map[string]*Profile `yaml:"profiles,omitempty"` 18 | } 19 | 20 | // NewPlatform returns an empty Platform object. 21 | func NewPlatform() *Platform { 22 | return &Platform{ 23 | SourceDisks: make([]string, 0), 24 | Profiles: make(map[string]*Profile), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /boxen/docker/commit.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | func Commit(opts ...Option) error { 10 | a := &args{} 11 | 12 | for _, o := range opts { 13 | err := o(a) 14 | 15 | if err != nil { 16 | return err 17 | } 18 | } 19 | 20 | if a.container == "" { 21 | panic("container id not provided, can't commit container") 22 | } 23 | 24 | if a.repo == "" || a.tag == "" { 25 | panic("repo and tag not provided, can't commit container") 26 | } 27 | 28 | cmdArgs := []string{"commit"} 29 | 30 | if a.commitChange != "" { 31 | cmdArgs = append(cmdArgs, fmt.Sprintf("--change=%s", a.commitChange)) 32 | } 33 | 34 | cmdArgs = append(cmdArgs, a.container, fmt.Sprintf("%s:%s", a.repo, a.tag)) 35 | 36 | executeArgs := setExecuteArgs(a) 37 | 38 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 39 | 40 | r, err := command.Execute(dockerCmd, executeArgs...) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = r.CheckStdErr() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return r.Proc.Wait() 51 | } 52 | -------------------------------------------------------------------------------- /boxen/cli/stop.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/carlmontanari/boxen/boxen/boxen" 7 | ) 8 | 9 | func Stop(config, instances string) error { 10 | err := checkSudo() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | l, li, err := spinLogger() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return spin(l, li, func() error { 26 | return instanceOp(b.Stop, instances) 27 | }) 28 | } 29 | 30 | func StopGroup(config, group string) error { 31 | err := checkSudo() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | l, li, err := spinLogger() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | instances, err := b.GetGroupInstances(group) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return spin(l, li, func() error { 52 | return instanceOp(b.Stop, strings.Join(instances, ",")) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Carl Montanari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /boxen/command/execute.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | 7 | "github.com/carlmontanari/boxen/boxen/util" 8 | ) 9 | 10 | type args struct { 11 | args []string 12 | workDir string 13 | sudo bool 14 | stdOut io.Writer 15 | stdErr io.Writer 16 | wait bool 17 | } 18 | 19 | func Execute(cmd string, opts ...ExecuteOption) (*Result, error) { 20 | a := &args{} 21 | 22 | for _, o := range opts { 23 | err := o(a) 24 | 25 | if err != nil { 26 | return nil, err 27 | } 28 | } 29 | 30 | if a.sudo { 31 | cmd, a.args = newSudoer().updateCmd(cmd, a.args) 32 | } 33 | 34 | r := &Result{ 35 | stdout: util.NewLockingWriterReader(), 36 | stderr: util.NewLockingWriterReader(), 37 | stderrInt: util.NewLockingWriterReader(), 38 | } 39 | 40 | r.Proc = exec.Command(cmd, a.args...) //nolint:gosec 41 | 42 | if a.workDir != "" { 43 | r.Proc.Dir = a.workDir 44 | } 45 | 46 | err := r.setIO(a) 47 | if err != nil { 48 | return r, err 49 | } 50 | 51 | err = r.Proc.Start() 52 | if err != nil { 53 | return r, err 54 | } 55 | 56 | if a.wait { 57 | err = r.Proc.Wait() 58 | } 59 | 60 | return r, err 61 | } 62 | -------------------------------------------------------------------------------- /boxen/platforms/common.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/carlmontanari/boxen/boxen/instance" 7 | "github.com/carlmontanari/boxen/boxen/util" 8 | ) 9 | 10 | func setStartArgs(opts ...instance.Option) (*startArgs, []instance.Option, error) { 11 | a := &startArgs{} 12 | 13 | var instanceOpts []instance.Option 14 | 15 | for _, option := range opts { 16 | err := option(a) 17 | 18 | if err != nil { 19 | if errors.Is(err, util.ErrIgnoredOption) { 20 | instanceOpts = append(instanceOpts, option) 21 | continue 22 | } else { 23 | return nil, nil, err 24 | } 25 | } 26 | } 27 | 28 | return a, instanceOpts, nil 29 | } 30 | 31 | func setInstallArgs(opts ...instance.Option) (*installArgs, []instance.Option, error) { 32 | a := &installArgs{} 33 | 34 | var instanceOpts []instance.Option 35 | 36 | for _, option := range opts { 37 | err := option(a) 38 | 39 | if err != nil { 40 | if errors.Is(err, util.ErrIgnoredOption) { 41 | instanceOpts = append(instanceOpts, option) 42 | continue 43 | } else { 44 | return nil, nil, err 45 | } 46 | } 47 | } 48 | 49 | return a, instanceOpts, nil 50 | } 51 | -------------------------------------------------------------------------------- /boxen/cli/deprovision.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/boxen" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func deProvisionCommands() []*cli.Command { 10 | config := boxenGlobalFlags() 11 | 12 | instances := &cli.StringFlag{ 13 | Name: "instances", 14 | Usage: "instance or comma sep string of instances to provision", 15 | Required: false, 16 | } 17 | 18 | return []*cli.Command{ 19 | { 20 | Name: "deprovision", 21 | Usage: "deprovision a local boxen instance", 22 | Flags: []cli.Flag{ 23 | config, 24 | instances, 25 | }, 26 | Action: func(c *cli.Context) error { 27 | return DeProvision( 28 | c.String("config"), 29 | c.String("instances"), 30 | ) 31 | }, 32 | }, 33 | } 34 | } 35 | 36 | func DeProvision(config, instances string) error { 37 | l, li, err := spinLogger() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return spin( 48 | l, 49 | li, 50 | func() error { return instanceOp(b.DeProvision, instances) }, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /boxen/docker/rm.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | ) 8 | 9 | func RmImage(repo, tag string, opts ...Option) error { 10 | a := &args{} 11 | 12 | for _, o := range opts { 13 | err := o(a) 14 | 15 | if err != nil { 16 | return err 17 | } 18 | } 19 | 20 | cmdArgs := []string{"image", "rm", fmt.Sprintf("%s:%s", repo, tag)} 21 | 22 | executeArgs := setExecuteArgs(a) 23 | 24 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 25 | 26 | r, err := command.Execute(dockerCmd, executeArgs...) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return r.Proc.Wait() 32 | } 33 | 34 | func RmContainer(container string, opts ...Option) error { 35 | a := &args{} 36 | 37 | for _, o := range opts { 38 | err := o(a) 39 | 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | cmdArgs := []string{"rm", container} 46 | 47 | executeArgs := setExecuteArgs(a) 48 | 49 | executeArgs = append(executeArgs, command.WithArgs(cmdArgs)) 50 | 51 | r, err := command.Execute(dockerCmd, executeArgs...) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return r.Proc.Wait() 57 | } 58 | -------------------------------------------------------------------------------- /boxen/platforms/timeouts.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import "github.com/carlmontanari/boxen/boxen/util" 4 | 5 | const ( 6 | // DefaultInstallTime is the default value for "installation" timeout -- this means handling 7 | // any of the initial prompt stuff and saving the disk. 8 | DefaultInstallTime = 600 9 | // DefaultBootTime default value for "bootup" time -- as in time before the console is ready 10 | // for inputs. 11 | DefaultBootTime = 360 12 | // DefaultSaveTime default value for saving configurations. 13 | DefaultSaveTime = 120 14 | ) 15 | 16 | func getPlatformBootTimeout(pT string) int { 17 | var t int 18 | 19 | switch pT { 20 | case PlatformTypeCiscoN9kv: 21 | t = ciscoN9kvDefaultBootTime 22 | case PlatformTypeCiscoXrv9k: 23 | t = ciscoXrv9kDefaultBootTime 24 | case PlatformTypePaloAltoPanos: 25 | t = paloAltoPanosDefaultBootTime 26 | case PlatformTypeCheckpointCloudguard: 27 | t = checkpointCloudGuardDefaultBootTime 28 | default: 29 | t = DefaultBootTime 30 | } 31 | 32 | return util.ApplyTimeoutMultiplier(t) 33 | } 34 | 35 | func getPlatformSaveTimeout(pT string) int { 36 | _ = pT 37 | 38 | t := DefaultSaveTime 39 | 40 | return util.ApplyTimeoutMultiplier(t) 41 | } 42 | -------------------------------------------------------------------------------- /boxen/instance/logging.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/carlmontanari/boxen/boxen/logging" 9 | ) 10 | 11 | type Loggers struct { 12 | // Base is the logger for "normal" logging -- meaning not stdout/stderr of the process and not 13 | // console output -- just for "boxen" logs. 14 | Base *logging.Instance 15 | // Stdout is the writer for qemu process stdout. 16 | Stdout io.Writer 17 | // Stderr is the writer for qemu process stderr. 18 | Stderr io.Writer 19 | // Console is the writer for console output (if applicable). 20 | Console io.Writer 21 | } 22 | 23 | func NewInstanceLoggersFOut(l *logging.Instance, d string) (*Loggers, error) { 24 | stdoutF, err := os.Create(fmt.Sprintf("%s/stdout.log", d)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | stderrF, err := os.Create(fmt.Sprintf("%s/stderr.log", d)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | consoleF, err := os.Create(fmt.Sprintf("%s/console.log", d)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | il := &Loggers{ 40 | Base: l, 41 | Stdout: stdoutF, 42 | Stderr: stderrF, 43 | Console: consoleF, 44 | } 45 | 46 | return il, nil 47 | } 48 | -------------------------------------------------------------------------------- /boxen/platforms/constants.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | const ( 4 | VendorCisco = "cisco" 5 | VendorArista = "arista" 6 | VendorJuniper = "juniper" 7 | VendorPaloAlto = "paloalto" 8 | VendorIPInfusion = "ipinfusion" 9 | VendorCheckpoint = "checkpoint" 10 | 11 | PlatformAristaVeos = "veos" 12 | PlatformCiscoCsr1000v = "csr1000v" 13 | PlatformCiscoXrv9k = "xrv9k" 14 | PlatformCiscoN9kv = "n9kv" 15 | PlatformJuniperVsrx = "vsrx" 16 | PlatformPaloAltoPanos = "panos" 17 | PlatformIPInfusionOcNOS = "ocnos" 18 | PlatformCheckpointCloudguard = "cloudguard" 19 | 20 | PlatformTypeAristaVeos = "arista_veos" 21 | PlatformTypeCiscoCsr1000v = "cisco_csr1000v" 22 | PlatformTypeCiscoXrv9k = "cisco_xrv9k" 23 | PlatformTypeCiscoN9kv = "cisco_n9kv" 24 | PlatformTypeJuniperVsrx = "juniper_vsrx" 25 | PlatformTypePaloAltoPanos = "paloalto_panos" 26 | PlatformTypeIPInfusionOcNOS = "ipinfusion_ocnos" 27 | PlatformTypeCheckpointCloudguard = "checkpoint_cloudguard" 28 | 29 | NicE1000 = "e1000" 30 | NicVirtio = "virtio-net-pci" 31 | 32 | AccelKVM = "kvm" 33 | AccelHVF = "hvf" 34 | AccelHAX = "hax" 35 | AccelNone = "none" 36 | ) 37 | -------------------------------------------------------------------------------- /boxen/cli/start.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/carlmontanari/boxen/boxen/boxen" 7 | ) 8 | 9 | // Start starts the provided instance(s) (provided as comma separated string). 10 | func Start(config, instances string) error { 11 | err := checkSudo() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | l, li, err := spinLogger() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | return spin(l, li, func() error { 27 | return instanceOp(b.Start, instances) 28 | }) 29 | } 30 | 31 | // StartGroup starts all local instances in a group. 32 | func StartGroup(config, group string) error { 33 | err := checkSudo() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | l, li, err := spinLogger() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | instances, err := b.GetGroupInstances(group) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return spin(l, li, func() error { 54 | return instanceOp(b.Start, strings.Join(instances, ",")) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /boxen/logging/writer.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "fmt" 4 | 5 | // InstanceIoWriter is a wrapper around an Instance such that that instance can be used anywhere an 6 | // io.Writer instance is required. 7 | type InstanceIoWriter struct { 8 | instance *Instance 9 | level string 10 | } 11 | 12 | type IoWriterOption func(writer *InstanceIoWriter) 13 | 14 | func WithIoWriterLevel(l string) IoWriterOption { 15 | return func(liw *InstanceIoWriter) { 16 | liw.level = l 17 | } 18 | } 19 | 20 | func NewInstanceIoWriter(li *Instance, opts ...IoWriterOption) *InstanceIoWriter { 21 | liw := &InstanceIoWriter{ 22 | instance: li, 23 | level: info, 24 | } 25 | 26 | for _, o := range opts { 27 | o(liw) 28 | } 29 | 30 | return liw 31 | } 32 | 33 | // Write accepts a log message that will be emitted to the embedded Instance log instance at the 34 | // provided log level. 35 | func (liw *InstanceIoWriter) Write(b []byte) (n int, err error) { 36 | var f func(b []byte) 37 | 38 | switch liw.level { 39 | case debug: 40 | f = liw.instance.Debugb 41 | case info: 42 | f = liw.instance.Infob 43 | case critical: 44 | f = liw.instance.Criticalb 45 | default: 46 | return -1, fmt.Errorf("%w: writer setup with invalid log level", ErrLogError) 47 | } 48 | 49 | f(b) 50 | 51 | return len(b), nil 52 | } 53 | -------------------------------------------------------------------------------- /boxen/boxen/mgmtnet.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import "github.com/carlmontanari/boxen/boxen/config" 4 | 5 | const ( 6 | // host port base number, instance ports are incremented from this value. 7 | hostPortBase = 48000 8 | ) 9 | 10 | // ZipPlatformProfileNats receives the lists of TCP and UDP ports 11 | // that are defined in a platform profile yaml. 12 | // For each received tcp/udp instance port a host port is allocated sequentially 13 | // from the hostPortBase port number. 14 | // These port mappings are used in packaging workflow (e.g. when building for containerlab). 15 | func ZipPlatformProfileNats( 16 | platformTCPPorts, platformUDPPorts []int, 17 | ) (tcpNats, udpNats []*config.NatPortPair) { 18 | tcpPortsLen := len(platformTCPPorts) 19 | udpPortsLen := len(platformUDPPorts) 20 | 21 | tcpNats = make([]*config.NatPortPair, 0, tcpPortsLen) 22 | udpNats = make([]*config.NatPortPair, 0, udpPortsLen) 23 | 24 | for i, tcp := range platformTCPPorts { 25 | tcpNats = append(tcpNats, &config.NatPortPair{ 26 | InstanceSide: tcp, 27 | HostSide: hostPortBase + i, 28 | }) 29 | } 30 | 31 | for i, udp := range platformUDPPorts { 32 | udpNats = append(udpNats, &config.NatPortPair{ 33 | InstanceSide: udp, 34 | HostSide: hostPortBase + tcpPortsLen + i, 35 | }) 36 | } 37 | 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /boxen/util/qemu.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "runtime" 7 | ) 8 | 9 | func kvmOk() bool { 10 | proc := exec.Command("kvm-ok") 11 | 12 | out, _ := proc.Output() 13 | 14 | if bytes.Contains(out, []byte("command not found")) { 15 | return false 16 | } else if bytes.Contains(out, []byte("KVM acceleration can NOT be used")) { 17 | return false 18 | } else if bytes.Contains(out, []byte("KVM acceleration can be used")) { 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | 25 | func haxOK() bool { 26 | proc := exec.Command("kextstat") 27 | 28 | out, err := proc.Output() 29 | 30 | if err != nil { 31 | return false 32 | } else if bytes.Contains(out, []byte("com.intel.kext.intelhaxm")) { 33 | return true 34 | } 35 | 36 | return false 37 | } 38 | 39 | func AvailableAccel() []string { 40 | availAccel := []string{"none"} 41 | 42 | if kvmOk() { 43 | availAccel = append(availAccel, "kvm") 44 | } 45 | 46 | if runtime.GOOS == "darwin" { 47 | availAccel = append(availAccel, "hvf") 48 | 49 | if haxOK() { 50 | availAccel = append(availAccel, "hax") 51 | } 52 | } 53 | 54 | return availAccel 55 | } 56 | 57 | func GetQemuPath() string { 58 | q, err := exec.LookPath("qemu-system-x86_64") 59 | if err != nil { 60 | return "" 61 | } 62 | 63 | return q 64 | } 65 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | BIN_DIR = $$(pwd)/bin 3 | BINARY = $$(pwd)/bin/boxen 4 | 5 | build: ## Build boxen 6 | mkdir -p $(BIN_DIR) 7 | go build -o $(BINARY) -ldflags="-s -w" main.go 8 | 9 | lint: ## Run linters 10 | gofmt -w -s . 11 | goimports -w . 12 | golines -w . 13 | golangci-lint run 14 | 15 | docker-lint: ## Run linters with docker 16 | docker run -it --rm -v $$(pwd):/work ghcr.io/hellt/golines:0.8.0 golines -w . 17 | docker run -it --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v1.45.0 golangci-lint run --timeout 5m -v 18 | 19 | test: ## Run unit tests 20 | gotestsum --format testname --hide-summary=skipped -- -coverprofile=cover.out ./... 21 | 22 | test-race: ## Run unit tests with race flag 23 | gotestsum --format testname --hide-summary=skipped -- -coverprofile=cover.out ./... -race 24 | 25 | ttl-push: build ## push locally built binary to ttl.sh container registry 26 | docker run --rm -v $$(pwd)/bin:/workspace ghcr.io/oras-project/oras:v0.12.0 push ttl.sh/boxen-$$(git rev-parse --short HEAD):1d ./boxen 27 | @echo "download with: docker run --rm -v \$$(pwd):/workspace ghcr.io/oras-project/oras:v0.12.0 pull ttl.sh/boxen-$$(git rev-parse --short HEAD):1d" 28 | 29 | help: 30 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -------------------------------------------------------------------------------- /boxen/util/lockingwriter.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // LockingWriterReader is a simple type that satisfies io.Writer, but also allows for reads. It does 9 | // this with a lock as well, so it is safe to use with long-running commands. It also allows for 10 | // reading from stderr (for example) while the process is still running without any race issues. 11 | type LockingWriterReader struct { 12 | buf *bytes.Buffer 13 | lock *sync.RWMutex 14 | } 15 | 16 | // NewLockingWriterReader returns a new instance of LockingWriterReader. 17 | func NewLockingWriterReader() *LockingWriterReader { 18 | return &LockingWriterReader{ 19 | buf: &bytes.Buffer{}, 20 | lock: &sync.RWMutex{}, 21 | } 22 | } 23 | 24 | // Write safely writes (with a lock) to the buffer in LockingWriterReader instance. 25 | func (lw *LockingWriterReader) Write(b []byte) (n int, err error) { 26 | lw.lock.Lock() 27 | defer lw.lock.Unlock() 28 | 29 | lw.buf.Write(b) 30 | 31 | return len(b), nil 32 | } 33 | 34 | // Read safely reads (with a rlock) from the buffer in LockingWriterReader instance. 35 | func (lw *LockingWriterReader) Read() ([]byte, error) { 36 | lw.lock.RLock() 37 | defer lw.lock.RUnlock() 38 | 39 | b := make([]byte, MaxBuffer) 40 | 41 | _, err := lw.buf.Read(b) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return b, nil 47 | } 48 | -------------------------------------------------------------------------------- /boxen/assets/packaging/build.Dockerfile.template: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -yq && \ 5 | apt-get -yq --no-install-recommends install \ 6 | ca-certificates \ 7 | bridge-utils=1.6* \ 8 | iproute2=5.5.0* \ 9 | socat=1.7.3* \ 10 | cpu-checker=0.7* \ 11 | curl=7.68.0* \ 12 | telnet=0.17-41* \ 13 | linux-image-generic=5.4.* \ 14 | libguestfs-tools=1:1.40* \ 15 | qemu-system-x86=1:4.2* && \ 16 | apt-get clean && \ 17 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/archive/*.deb 18 | 19 | ENV BOXEN_TIMEOUT_MULTIPLIER={{ .TimeoutMultiplier }} 20 | ENV BOXEN_LOG_TARGET={{ $.LocalHost }}:6667 21 | ENV BOXEN_LOG_LEVEL=debug 22 | ENV BOXEN_SPARSIFY_DISK={{ $.Sparsify }} 23 | 24 | COPY tc-tap-ifup /etc/ 25 | RUN chmod 0777 /etc/tc-tap-ifup 26 | COPY boxen.yaml / 27 | 28 | {{if not .BinaryOverride }} 29 | RUN bash -c "$(curl -sL https://raw.githubusercontent.com/carlmontanari/boxen/main/get.sh)" -- -v {{ .BoxenVersion }} 30 | {{else}} 31 | RUN curl http://{{ $.LocalHost }}:6666/boxen -o boxen 32 | RUN chmod +x boxen && mv boxen /usr/local/bin/boxen 33 | {{end}} 34 | 35 | {{range $index, $file := .RequiredFiles -}} 36 | RUN curl http://{{ $.LocalHost }}:6666/{{$file}} -o {{$file}} 37 | {{end}} 38 | 39 | ENTRYPOINT ["boxen", "package-install"] -------------------------------------------------------------------------------- /boxen/boxen/defaults.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/config" 5 | ) 6 | 7 | func defaultTCPNatMap() map[int]int { 8 | return map[int]int{ 9 | 22: 21022, 10 | 23: 21023, 11 | 80: 21080, 12 | 443: 21443, 13 | 830: 21830, 14 | } 15 | } 16 | 17 | func zipDefaultTCPNats(platformDefaultNats []int) []*config.NatPortPair { 18 | nats := make([]*config.NatPortPair, 0) 19 | 20 | defaultNATMap := defaultTCPNatMap() 21 | 22 | for _, instancePort := range platformDefaultNats { 23 | hostPort, ok := defaultNATMap[instancePort] 24 | if !ok { 25 | continue 26 | } 27 | 28 | nats = append(nats, &config.NatPortPair{ 29 | InstanceSide: instancePort, 30 | HostSide: hostPort, 31 | }) 32 | } 33 | 34 | return nats 35 | } 36 | 37 | func defaultUDPNatMap() map[int]int { 38 | return map[int]int{ 39 | 161: 31161, 40 | } 41 | } 42 | 43 | func zipDefaultUDPNats(platformDefaultNats []int) []*config.NatPortPair { 44 | nats := make([]*config.NatPortPair, 0) 45 | 46 | defaultNATMap := defaultUDPNatMap() 47 | 48 | for _, instancePort := range platformDefaultNats { 49 | hostPort, ok := defaultNATMap[instancePort] 50 | if !ok { 51 | continue 52 | } 53 | 54 | nats = append(nats, &config.NatPortPair{ 55 | InstanceSide: instancePort, 56 | HostSide: hostPort, 57 | }) 58 | } 59 | 60 | return nats 61 | } 62 | -------------------------------------------------------------------------------- /boxen/boxen/configs.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "os" 8 | "strings" 9 | 10 | "github.com/carlmontanari/boxen/boxen" 11 | ) 12 | 13 | type configTemplateArgs struct { 14 | Username string 15 | Password string 16 | SecondaryPassword string 17 | } 18 | 19 | // RenderInitialConfig renders the initial installation config template. 20 | func (b *Boxen) RenderInitialConfig( 21 | name string, 22 | ) ([]string, error) { 23 | platformType := b.Config.Instances[name].PlatformType 24 | 25 | templateData := &configTemplateArgs{ 26 | Username: b.Config.Instances[name].Credentials.Username, 27 | Password: b.Config.Instances[name].Credentials.Password, 28 | } 29 | 30 | var t *template.Template 31 | 32 | var err error 33 | 34 | envProfilePath := os.Getenv( 35 | fmt.Sprintf("BOXEN_%s_INITIAL_CONFIG_TEMPLATE", strings.ToUpper(platformType)), 36 | ) 37 | if envProfilePath != "" { 38 | t, err = template.ParseFiles(envProfilePath) 39 | } else { 40 | t, err = template.ParseFS( 41 | boxen.Assets, 42 | fmt.Sprintf("assets/configs/%s.template", platformType), 43 | ) 44 | } 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | var rendered bytes.Buffer 51 | 52 | err = t.Execute(&rendered, templateData) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return strings.Split(rendered.String(), "\n"), nil 58 | } 59 | -------------------------------------------------------------------------------- /boxen/assets/packaging/Dockerfile.template: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -yq && \ 5 | apt-get -yq --no-install-recommends install \ 6 | ca-certificates \ 7 | bridge-utils=1.6* \ 8 | iproute2=5.5.0* \ 9 | socat=1.7.3* \ 10 | cpu-checker=0.7* \ 11 | curl=7.68.0* \ 12 | qemu-system-x86=1:4.2* && \ 13 | apt-get clean && \ 14 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/archive/*.deb 15 | 16 | ENV BOXEN_TIMEOUT_MULTIPLIER={{ .TimeoutMultiplier }} 17 | ENV BOXEN_LOG_LEVEL={{ .LogLevel }} 18 | 19 | COPY tc-tap-ifup /etc/ 20 | RUN chmod 0777 /etc/tc-tap-ifup 21 | COPY boxen.yaml / 22 | 23 | {{if not .BinaryOverride }} 24 | RUN bash -c "$(curl -sL https://raw.githubusercontent.com/carlmontanari/boxen/main/get.sh)" -- -v {{ .BoxenVersion }} 25 | {{else}} 26 | RUN curl http://{{ $.LocalHost }}:6666/boxen -o boxen 27 | RUN chmod +x boxen && mv boxen /usr/local/bin/boxen 28 | {{end}} 29 | 30 | {{range $index, $file := .RequiredFiles -}} 31 | RUN curl http://{{ $.LocalHost }}:6666/{{$file}} -o {{$file}} 32 | {{end}} 33 | 34 | # expose monitor and console ports 35 | EXPOSE 4001 5001 36 | 37 | # expose ports from device profile 38 | EXPOSE {{range $index, $port := .ExposedTCPPorts -}}{{$port}} {{end}} 39 | EXPOSE {{range $index, $port := .ExposedUDPPorts -}}{{$port}}/udp {{end}} 40 | 41 | HEALTHCHECK CMD curl --fail http://localhost:7777 || exit 1 42 | 43 | ENTRYPOINT ["boxen", "package-start"] -------------------------------------------------------------------------------- /boxen/logging/socketsender.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // SocketSender is an object that can emit log messages via TCP socket to a SocketReceiver. 10 | type SocketSender struct { 11 | *ConnData 12 | c net.Conn 13 | } 14 | 15 | // NewSocketSender returns a new instance of SocketSender with the TCP connection opened. 16 | func NewSocketSender(a string, p int) (*SocketSender, error) { 17 | ss := &SocketSender{ 18 | &ConnData{ 19 | Protocol: tcp, 20 | Address: a, 21 | Port: p, 22 | }, 23 | nil, 24 | } 25 | 26 | err := ss.open() 27 | 28 | return ss, err 29 | } 30 | 31 | // open the TCP session to the SocketReceiver. 32 | func (ss *SocketSender) open() error { 33 | var err error 34 | 35 | ss.c, err = net.Dial(ss.Protocol, fmt.Sprintf("%s:%d", ss.Address, ss.Port)) 36 | if err != nil { 37 | return ErrSocketFailure 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Emit sends log messages to the SocketReceiver -- intended to be used with NewInstance as the 44 | // Logger attribute. 45 | func (ss *SocketSender) Emit(o ...interface{}) { 46 | if len(o) == 0 { 47 | return 48 | } 49 | 50 | s, ok := o[0].(string) 51 | if !ok { 52 | return 53 | } 54 | 55 | if !strings.HasSuffix(s, "\n") { 56 | s = fmt.Sprintf("%s\n", s) 57 | } 58 | 59 | _, err := ss.c.Write([]byte(s)) 60 | if err != nil { 61 | panic(fmt.Sprintf("error emitting message to socket %s\n", err)) 62 | } 63 | } 64 | 65 | // Close the SocketSender. 66 | func (ss *SocketSender) Close() error { 67 | return ss.c.Close() 68 | } 69 | -------------------------------------------------------------------------------- /boxen/util/errors.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "errors" 4 | 5 | // ErrIgnoredOption is an error returned when any option is not applicable for the given operation 6 | // -- when this error is returned it is *usually* safe to ignore. 7 | var ErrIgnoredOption = errors.New("ignoredOption") 8 | 9 | // ErrInspectionError is an error returned when an issue "inspecting" an instance is encountered. 10 | var ErrInspectionError = errors.New("inspectionError") 11 | 12 | // ErrValidationError is an error returned when validating provided information, usually from a 13 | // user, is encountered. 14 | var ErrValidationError = errors.New("validationError") 15 | 16 | // ErrAllocationError is an error returned when boxen encounters an issue allocating resources to a 17 | // virtual machine. 18 | var ErrAllocationError = errors.New("allocationError") 19 | 20 | // ErrProvisionError is an error returned when provisioning a virtual machine in the local 21 | // configuration fails. 22 | var ErrProvisionError = errors.New("provisionError") 23 | 24 | // ErrCommandError is an error returned when a command fails to execute. 25 | var ErrCommandError = errors.New("commandError") 26 | 27 | // ErrInstanceError is an error returned when a "well-formed"/created instance (as in, an instance 28 | // that gets to the point of starting) encounters an error. 29 | var ErrInstanceError = errors.New("instanceError") 30 | 31 | // ErrConsoleError is an error returned when connecting to a device console produces an error, or 32 | // when a console operation fails. 33 | var ErrConsoleError = errors.New("consoleError") 34 | -------------------------------------------------------------------------------- /boxen/platforms/options.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/instance" 5 | "github.com/carlmontanari/boxen/boxen/util" 6 | ) 7 | 8 | type installArgs struct { 9 | configLines []string 10 | } 11 | 12 | // WithInstallConfig sets an option to push configs during platform installation. 13 | func WithInstallConfig(configLines []string) instance.Option { 14 | return func(o interface{}) error { 15 | a, ok := o.(*installArgs) 16 | 17 | if ok { 18 | a.configLines = configLines 19 | return nil 20 | } 21 | 22 | return util.ErrIgnoredOption 23 | } 24 | } 25 | 26 | type startArgs struct { 27 | prepareConsole bool 28 | runUntilSigint bool 29 | } 30 | 31 | // WithPrepareConsole sets an option to notify the platform implementation to prepare the console 32 | // connection to receive configurations/commands. This typically will mean handling any initial 33 | // login after a "start ready" state, and disabling paging and the like. 34 | func WithPrepareConsole(b bool) instance.Option { 35 | return func(o interface{}) error { 36 | a, ok := o.(*startArgs) 37 | 38 | if ok { 39 | a.prepareConsole = b 40 | return nil 41 | } 42 | 43 | return util.ErrIgnoredOption 44 | } 45 | } 46 | 47 | // WithRunUntilSigint sets an option to launch health check and run until signal interrupt. 48 | func WithRunUntilSigint(b bool) instance.Option { 49 | return func(o interface{}) error { 50 | a, ok := o.(*startArgs) 51 | 52 | if ok { 53 | a.runUntilSigint = b 54 | return nil 55 | } 56 | 57 | return util.ErrIgnoredOption 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /boxen/cli/common.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/carlmontanari/boxen/boxen/command" 8 | ) 9 | 10 | // checkSudo executes a command (with sudo) to check if the calling user is a super-user and if they 11 | // have passwordless sudo permissions -- if they do not, the command package will prompt the user 12 | // for their sudo password. We do this in the cli module prior to the "spin" starting so that we 13 | // don't need to interrupt the spin to prompt for passwords. This function should be called prior to 14 | // any operations that may require elevated permissions, tasks requiring elevated permissions are: 15 | // - packagebuild (required to run privileged containers) 16 | // - install (because qemu may require elevated permissions for kvm/taps/nat interfaces/etc.) 17 | // - start (same as install) 18 | // - stop (because we launch qemu instances w/ sudo) 19 | func checkSudo() error { 20 | _, err := command.Execute( 21 | "pwd", 22 | command.WithSudo(true), 23 | command.WithWait(true), 24 | ) 25 | 26 | return err 27 | } 28 | 29 | func instanceOp(f func(string) error, instances string) error { 30 | wg := &sync.WaitGroup{} 31 | 32 | instanceSlice := strings.Split(instances, ",") 33 | 34 | var errs []error 35 | 36 | for _, instance := range instanceSlice { 37 | wg.Add(1) 38 | 39 | i := instance 40 | 41 | go func() { 42 | err := f(i) 43 | 44 | if err != nil { 45 | errs = append(errs, err) 46 | } 47 | 48 | wg.Done() 49 | }() 50 | } 51 | 52 | wg.Wait() 53 | 54 | if len(errs) > 0 { 55 | return errs[0] 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /boxen/cli/install.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/boxen" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func installCommands() []*cli.Command { 10 | config := boxenGlobalFlags() 11 | 12 | disk := &cli.StringFlag{ 13 | Name: "disk", 14 | Usage: "disk image to target, ex: 'vEOS-lab-4.22.1F.vmdk'", 15 | Required: true, 16 | } 17 | 18 | username, password, _ := customizationFlags() 19 | vendor, platform, version := platformTargetFlags() 20 | 21 | return []*cli.Command{{ 22 | Name: "install", 23 | Usage: "install a source disk for local boxen instances", 24 | Flags: []cli.Flag{ 25 | config, 26 | disk, 27 | username, 28 | password, 29 | vendor, 30 | platform, 31 | version, 32 | }, 33 | Action: func(c *cli.Context) error { 34 | return Install( 35 | c.String("config"), 36 | c.String("disk"), 37 | c.String("username"), 38 | c.String("password"), 39 | c.String("vendor"), 40 | c.String("platform"), 41 | c.String("version"), 42 | ) 43 | }, 44 | }} 45 | } 46 | 47 | // Install is the cli entrypoint to install a disk as a local source disk. 48 | func Install(config, disk, username, password, vendor, platform, version string) error { 49 | err := checkSudo() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | l, li, err := spinLogger() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return spin( 65 | l, 66 | li, 67 | func() error { return b.Install(disk, username, password, vendor, platform, version) }, 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /boxen/cli/spin.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/carlmontanari/boxen/boxen/logging" 9 | "github.com/carlmontanari/boxen/boxen/util" 10 | ) 11 | 12 | func spinLogger() (*logging.FifoLogQueue, *logging.Instance, error) { 13 | logLevel := util.GetEnvStrOrDefault("BOXEN_LOG_LEVEL", "info") 14 | 15 | l := logging.NewFifoLogQueue() 16 | li, err := logging.NewInstance(l.Accept, logging.WithLevel(logLevel)) 17 | 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | 22 | return l, li, nil 23 | } 24 | 25 | func spin(l *logging.FifoLogQueue, li *logging.Instance, f func() error) error { 26 | startTime := time.Now() 27 | 28 | c := make(chan error) 29 | 30 | s := util.NewSpinner() 31 | s.PostUpdate = func(s *util.Spinner) { 32 | elapsed := int(math.Round(time.Since(startTime).Seconds())) 33 | s.Prefix = l.Emit() 34 | s.Suffix = fmt.Sprintf(" %d seconds elapsed", elapsed) 35 | } 36 | 37 | s.Start() 38 | 39 | go func() { 40 | c <- f() 41 | }() 42 | 43 | err := <-c 44 | 45 | s.Finale = func(s *util.Spinner) string { 46 | symbol := "✅" 47 | state := "successfully" 48 | 49 | if err != nil { 50 | symbol = "🆘" 51 | state = "unsuccessfully" 52 | } 53 | 54 | li.Drain() 55 | 56 | elapsed := int(math.Round(time.Since(startTime).Seconds())) 57 | s.Prefix = l.Emit() 58 | s.Suffix = fmt.Sprintf("finished %s in %d seconds", state, elapsed) 59 | 60 | out := fmt.Sprintf("\r%s\n\t%s %s ", s.Prefix, symbol, s.Suffix) 61 | if s.Prefix == "" { 62 | out = fmt.Sprintf("\r%s\t%s %s ", s.Prefix, symbol, s.Suffix) 63 | } 64 | 65 | return out 66 | } 67 | 68 | s.Stop() 69 | 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /boxen/command/options.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | type ExecuteOption func(*args) error 9 | 10 | func WithArgs(a []string) ExecuteOption { 11 | return func(o *args) error { 12 | o.args = a 13 | 14 | return nil 15 | } 16 | } 17 | 18 | func WithWorkDir(workDir string) ExecuteOption { 19 | return func(o *args) error { 20 | o.workDir = workDir 21 | 22 | return nil 23 | } 24 | } 25 | 26 | func WithSudo(sudo bool) ExecuteOption { 27 | return func(o *args) error { 28 | o.sudo = sudo 29 | 30 | return nil 31 | } 32 | } 33 | 34 | func WithStdOut(stdout io.Writer) ExecuteOption { 35 | return func(o *args) error { 36 | o.stdOut = stdout 37 | 38 | return nil 39 | } 40 | } 41 | 42 | func WithStdErr(stderr io.Writer) ExecuteOption { 43 | return func(o *args) error { 44 | o.stdErr = stderr 45 | 46 | return nil 47 | } 48 | } 49 | 50 | func WithWait(wait bool) ExecuteOption { 51 | return func(o *args) error { 52 | o.wait = wait 53 | 54 | return nil 55 | } 56 | } 57 | 58 | type CheckOption func(*checkArgs) error 59 | 60 | func WithIgnore(i [][]byte) CheckOption { 61 | return func(o *checkArgs) error { 62 | o.ignore = i 63 | 64 | return nil 65 | } 66 | } 67 | 68 | func WithIsError(i [][]byte) CheckOption { 69 | return func(o *checkArgs) error { 70 | o.isError = i 71 | 72 | return nil 73 | } 74 | } 75 | 76 | func WithDuration(t time.Duration) CheckOption { 77 | return func(o *checkArgs) error { 78 | o.duration = t 79 | 80 | return nil 81 | } 82 | } 83 | 84 | func WithInterval(t time.Duration) CheckOption { 85 | return func(o *checkArgs) error { 86 | o.interval = t 87 | 88 | return nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /boxen/logging/message.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sync" 7 | ) 8 | 9 | // Message is a simple struct that contains some fields relevant for log messages within boxen. 10 | type Message struct { 11 | Message string 12 | Level string 13 | Timestamp string 14 | } 15 | 16 | // MessageFormatter is an interface that defines the requirements for a (future) optional message 17 | // formatter that can be set on logging Instance objects. 18 | type MessageFormatter interface { 19 | Encode(lm *Message) string 20 | Decode(s string) *Message 21 | } 22 | 23 | // DefaultFormatter is a simple log MessageFormatter implementation. 24 | type DefaultFormatter struct { 25 | decodeRe *regexp.Regexp 26 | compileOnce sync.Once 27 | } 28 | 29 | func (df *DefaultFormatter) Encode(lm *Message) string { 30 | return fmt.Sprintf("%10s %12s %s", lm.Level, lm.Timestamp, lm.Message) 31 | } 32 | 33 | func (df *DefaultFormatter) Decode(s string) *Message { 34 | df.compileOnce.Do(func() { 35 | df.decodeRe = regexp.MustCompile(`(?mis)^\s+(\w{4,8})\s+(\d{10})\s(.*)`) 36 | }) 37 | 38 | parts := df.decodeRe.FindStringSubmatch(s) 39 | 40 | if len(parts) != 4 { //nolint:gomnd 41 | panic("cannot decode message") 42 | } 43 | 44 | return &Message{ 45 | Message: parts[3], 46 | Level: parts[1], 47 | Timestamp: parts[2], 48 | } 49 | } 50 | 51 | // NoopFormatter is a MessageFormatter implementation that does nothing but pass the message. 52 | type NoopFormatter struct{} 53 | 54 | func (df *NoopFormatter) Encode(lm *Message) string { 55 | return lm.Message 56 | } 57 | 58 | func (df *NoopFormatter) Decode(s string) *Message { 59 | return &Message{ 60 | Message: s, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /boxen/cli/packageinstall.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/carlmontanari/boxen/boxen/boxen" 10 | "github.com/carlmontanari/boxen/boxen/logging" 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func packageInstallCommands() []*cli.Command { 17 | return []*cli.Command{{ 18 | Name: "package-install", 19 | Usage: "install/finalize a vm instance packaging in a container", 20 | Hidden: true, 21 | Action: func(c *cli.Context) error { 22 | return packageInstall() 23 | }, 24 | }} 25 | } 26 | 27 | func packageInstall() error { 28 | v, ok := os.LookupEnv("BOXEN_LOG_TARGET") 29 | if !ok { 30 | // for this block and the rest, we actually do want to panic since that will kill the 31 | // container, dump the panic output to container logs, and ultimately bubble the error back 32 | // up to the cli process that is managing the installation. 33 | panic("no boxen log target set, this shouldn't happen!") 34 | } 35 | 36 | parts := strings.Split(v, ":") 37 | a := parts[0] 38 | p, _ := strconv.Atoi(parts[1]) 39 | 40 | sl, err := logging.NewSocketSender(a, p) 41 | if err != nil { 42 | panic("could not setup socket sender") 43 | } 44 | 45 | logLevel := util.GetEnvStrOrDefault("BOXEN_LOG_LEVEL", "info") 46 | 47 | li, err := logging.NewInstance(sl.Emit, logging.WithLevel(logLevel)) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig("boxen.yaml")) 53 | if err != nil { 54 | panic(fmt.Sprintf("error spawning boxen instance: %s\n", err)) 55 | } 56 | 57 | err = b.PackageInstall() 58 | 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /boxen/boxen/init.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/carlmontanari/boxen/boxen/config" 8 | "github.com/carlmontanari/boxen/boxen/util" 9 | ) 10 | 11 | // Init initializes a boxen directory structure. 12 | func (b *Boxen) Init(d string) error { 13 | b.Logger.Infof("init boxen directory requested for directory '%s'", d) 14 | 15 | d = util.ExpandPath(d) 16 | b.Logger.Debugf("requested directory resolved as '%s'", d) 17 | 18 | if util.DirectoryExists(d) { 19 | return fmt.Errorf( 20 | "%w: requested directory '%s' already exists, cannot continue", 21 | util.ErrAllocationError, 22 | d, 23 | ) 24 | } 25 | 26 | instanceD := fmt.Sprintf("%s/instances", d) 27 | sourceD := fmt.Sprintf("%s/source", d) 28 | 29 | err := os.Mkdir(d, os.ModePerm) 30 | if err != nil { 31 | b.Logger.Criticalf("error creating requested directory: %s", err) 32 | 33 | return err 34 | } 35 | 36 | err = os.Mkdir(instanceD, os.ModePerm) 37 | if err != nil { 38 | b.Logger.Criticalf("error creating instance directory: %s", err) 39 | return err 40 | } 41 | 42 | err = os.Mkdir(sourceD, os.ModePerm) 43 | if err != nil { 44 | b.Logger.Criticalf("error creating source directory: %s", err) 45 | return err 46 | } 47 | 48 | b.Logger.Debug("boxen directories created successfully") 49 | 50 | b.Config = config.NewConfig() 51 | 52 | b.Config.Options.Build.InstancePath = instanceD 53 | b.Config.Options.Build.SourcePath = sourceD 54 | 55 | err = b.Config.Dump(fmt.Sprintf("%s/boxen.yaml", d)) 56 | if err != nil { 57 | b.Logger.Criticalf("error dumping boxen initial config to disk: %s", err) 58 | return err 59 | } 60 | 61 | b.Logger.Info("init boxen completed successfully") 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /boxen/config/hardware.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ProfileHardware struct { 4 | Memory int `yaml:"memory,omitempty"` 5 | Acceleration []string `yaml:"acceleration,omitempty"` 6 | SerialPortCount int `yaml:"serial_port_count,omitempty"` 7 | NicType string `yaml:"nic_type,omitempty"` 8 | NicCount int `yaml:"nic_count,omitempty"` 9 | NicPerBus int `yaml:"nic_per_bus,omitempty"` 10 | } 11 | 12 | func (p *ProfileHardware) ToHardware() *Hardware { 13 | // especially for packaging we basically just roll w/ the default profile for bootstrapping, but 14 | // because hardware profile has *count* of serial instead of actual serial ports we can't use 15 | // `ProfileHardware` as `Hardware` -- so this method just converts and drops the serial ports 16 | return &Hardware{ 17 | Memory: p.Memory, 18 | Acceleration: p.Acceleration, 19 | NicType: p.NicType, 20 | NicCount: p.NicCount, 21 | NicPerBus: p.NicPerBus, 22 | } 23 | } 24 | 25 | type Hardware struct { 26 | Memory int `yaml:"memory,omitempty"` 27 | Acceleration []string `yaml:"acceleration,omitempty"` 28 | MonitorPort int `yaml:"monitor_port,omitempty"` 29 | SerialPorts []int `yaml:"serial_ports,omitempty"` 30 | NicType string `yaml:"nic_type,omitempty"` 31 | NicCount int `yaml:"nic_count,omitempty"` 32 | NicPerBus int `yaml:"nic_per_bus,omitempty"` 33 | } 34 | 35 | type Advanced struct { 36 | Display string `yaml:"display,omitempty"` 37 | Machine string `yaml:"machine,omitempty"` 38 | CPU *AdvancedCPU `yaml:"cpu,omitempty"` 39 | } 40 | 41 | type AdvancedCPU struct { 42 | Emulation string `yaml:"emulation,omitempty"` 43 | Cores int `yaml:"cores,omitempty"` 44 | Threads int `yaml:"threads,omitempty"` 45 | Sockets int `yaml:"sockets,omitempty"` 46 | } 47 | -------------------------------------------------------------------------------- /boxen/cli/uninstall.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/carlmontanari/boxen/boxen/boxen" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func unInstallCommands() []*cli.Command { 13 | config := boxenGlobalFlags() 14 | 15 | platform := &cli.StringFlag{ 16 | Name: "platform", 17 | Usage: "boxen platform type, i.e. 'arista_veos' or 'cisco_csr1000v'", 18 | Required: true, 19 | } 20 | 21 | disk := &cli.StringFlag{ 22 | Name: "disks", 23 | Usage: "comma sep list of disk versions to uninstall, i.e. '4.22.1F,4.22.2F'", 24 | Required: true, 25 | } 26 | 27 | return []*cli.Command{{ 28 | Name: "uninstall", 29 | Usage: "uninstall source disk(s) for local boxen instances", 30 | Flags: []cli.Flag{ 31 | config, 32 | platform, 33 | disk, 34 | }, 35 | Action: func(c *cli.Context) error { 36 | return UnInstall( 37 | c.String("config"), 38 | c.String("platform"), 39 | c.String("disks"), 40 | ) 41 | }, 42 | }} 43 | } 44 | 45 | func UnInstall(config, pT, disks string) error { 46 | l, li, err := spinLogger() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return spin( 57 | l, 58 | li, func() error { 59 | wg := &sync.WaitGroup{} 60 | 61 | diskSlice := strings.Split(disks, ",") 62 | 63 | var errs []error 64 | 65 | for _, disk := range diskSlice { 66 | wg.Add(1) 67 | 68 | d := disk 69 | 70 | go func() { 71 | err = b.UnInstall(pT, d) 72 | 73 | if err != nil { 74 | errs = append(errs, err) 75 | } 76 | 77 | wg.Done() 78 | }() 79 | } 80 | 81 | wg.Wait() 82 | 83 | if len(errs) > 0 { 84 | return errs[0] 85 | } 86 | 87 | return nil 88 | }, 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /boxen/cli/packagebuild.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/boxen" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func packageBuildCommands() []*cli.Command { 10 | username, password, _ := customizationFlags() 11 | 12 | disk := &cli.StringFlag{ 13 | Name: "disk", 14 | Usage: "disk image to target, ex: 'vEOS-lab-4.22.1F.vmdk'", 15 | Required: true, 16 | } 17 | 18 | repo := &cli.StringFlag{ 19 | Name: "repo", 20 | Usage: "name of repository to tag packaged instance to", 21 | Required: false, 22 | } 23 | 24 | tag := &cli.StringFlag{ 25 | Name: "tag", 26 | Usage: "version tag to tag packaged instance to", 27 | Required: false, 28 | } 29 | 30 | vendor, platform, version := platformTargetFlags() 31 | 32 | return []*cli.Command{{ 33 | Name: "package", 34 | Usage: "package a vm instance as a container", 35 | Flags: []cli.Flag{ 36 | disk, 37 | username, 38 | password, 39 | repo, 40 | tag, 41 | vendor, 42 | platform, 43 | version, 44 | }, 45 | Action: func(c *cli.Context) error { 46 | return packageBuild( 47 | c.String("disk"), 48 | c.String("username"), 49 | c.String("password"), 50 | c.String("repo"), 51 | c.String("tag"), 52 | c.String("vendor"), 53 | c.String("platform"), 54 | c.String("version"), 55 | ) 56 | }, 57 | }} 58 | } 59 | 60 | func packageBuild(disk, username, password, repo, tag, vendor, platform, version string) error { 61 | err := checkSudo() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | l, li, err := spinLogger() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | b, err := boxen.NewBoxen(boxen.WithLogger(li)) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | return spin( 77 | l, 78 | li, 79 | func() error { 80 | return b.PackageBuild(disk, username, password, repo, tag, vendor, platform, version) 81 | }, 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /boxen/boxen/uninstall.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/carlmontanari/boxen/boxen/util" 9 | ) 10 | 11 | // UnInstall removes an installed source disk from the local boxen config. 12 | func (b *Boxen) UnInstall(pT, disk string) error { 13 | b.Logger.Infof("uninstall disk '%s' for platform type '%s' requested", disk, pT) 14 | 15 | _, ok := b.Config.Platforms[pT] 16 | if !ok { 17 | msg := fmt.Sprintf( 18 | "no disks for platform '%s' in config, cannot continue", 19 | pT, 20 | ) 21 | 22 | b.Logger.Critical(msg) 23 | 24 | return fmt.Errorf( 25 | "%w: %s", 26 | util.ErrAllocationError, 27 | msg, 28 | ) 29 | } 30 | 31 | err := os.RemoveAll( 32 | fmt.Sprintf( 33 | "%s/%s/%s", 34 | b.Config.Options.Build.SourcePath, 35 | pT, 36 | disk, 37 | ), 38 | ) 39 | if err != nil { 40 | b.Logger.Criticalf("error deleting installation files: %s", err) 41 | 42 | return err 43 | } 44 | 45 | updatedSourceDisks := make([]string, 0) 46 | 47 | for _, d := range b.Config.Platforms[pT].SourceDisks { 48 | if !strings.HasPrefix(d, disk) { 49 | updatedSourceDisks = append(updatedSourceDisks, d) 50 | } 51 | } 52 | 53 | b.Config.Platforms[pT].SourceDisks = updatedSourceDisks 54 | 55 | if len(b.Config.Platforms[pT].SourceDisks) == 0 { 56 | b.Logger.Debug("no disks remain for platform, deleting platform source directory") 57 | 58 | delete(b.Config.Platforms, pT) 59 | 60 | // also delete the platform dir in the source path if there are no more disks remaining 61 | // for the given platform type 62 | err = os.RemoveAll( 63 | fmt.Sprintf("%s/%s", b.Config.Options.Build.SourcePath, pT), 64 | ) 65 | if err != nil { 66 | b.Logger.Criticalf("error deleting source disk directory: %s", err) 67 | 68 | return err 69 | } 70 | } 71 | 72 | err = b.Config.Dump(b.ConfigPath) 73 | if err != nil { 74 | b.Logger.Criticalf("error dumping updated boxen config to disk: %s", err) 75 | return err 76 | } 77 | 78 | b.Logger.Infof("uninstall disk '%s' for platform type '%s' completed successfully", disk, pT) 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /boxen/logging/socketreceiver.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // SocketReceiver is an object that receives messages from the SocketSender and emits them to a 14 | // logging Instance. 15 | type SocketReceiver struct { 16 | *ConnData 17 | li *Instance 18 | l net.Listener 19 | conns *sync.Map 20 | } 21 | 22 | // NewSocketReceiver returns a SocketReceiver that is ready to receive messages. 23 | func NewSocketReceiver(a string, p int, li *Instance) (*SocketReceiver, error) { 24 | sr := &SocketReceiver{ 25 | &ConnData{ 26 | Protocol: tcp, 27 | Address: a, 28 | Port: p, 29 | }, 30 | li, 31 | nil, 32 | &sync.Map{}, 33 | } 34 | 35 | err := sr.listen() 36 | 37 | return sr, err 38 | } 39 | 40 | // queue messages received over the TCP connection into the logging Instance. 41 | func (sr *SocketReceiver) queue(s string) { 42 | lm := sr.li.Formatter.Decode(s) 43 | sr.li.queueMsg(lm) 44 | } 45 | 46 | // handle listens for messages and dispatches them appropriately to queue. 47 | func (sr *SocketReceiver) handle(id string, c net.Conn) { 48 | defer func() { 49 | c.Close() 50 | sr.conns.Delete(id) 51 | }() 52 | 53 | for { 54 | scanner := bufio.NewScanner(c) 55 | 56 | for scanner.Scan() { 57 | sr.queue(scanner.Text()) 58 | } 59 | 60 | time.Sleep(dequeInterval * time.Millisecond) 61 | } 62 | } 63 | 64 | // listen starts the SocketReceiver listening for messages from a SocketSender. 65 | func (sr *SocketReceiver) listen() error { 66 | var err error 67 | 68 | sr.l, err = net.Listen(sr.Protocol, fmt.Sprintf("%s:%d", sr.Address, sr.Port)) 69 | if err != nil { 70 | return ErrSocketFailure 71 | } 72 | 73 | go func() { 74 | for { 75 | conn, acceptErr := sr.l.Accept() 76 | if acceptErr != nil { 77 | // seems like we get a lot of these errors, but they have not had any impact... 78 | continue 79 | } 80 | 81 | id := uuid.New().String() 82 | sr.conns.Store(id, conn) 83 | 84 | go sr.handle(id, conn) 85 | } 86 | }() 87 | 88 | return nil 89 | } 90 | 91 | // Close the SocketReceiver. 92 | func (sr *SocketReceiver) Close() error { 93 | return sr.l.Close() 94 | } 95 | -------------------------------------------------------------------------------- /boxen/cli/packagestart.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/carlmontanari/boxen/boxen/boxen" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func vrnetlabFlags() ( //nolint: gocritic 10 | *cli.StringFlag, *cli.BoolFlag, *cli.StringFlag, *cli.StringFlag, 11 | ) { 12 | vrConnectionMode := &cli.StringFlag{ 13 | Name: "connection-mode", 14 | Usage: "ignored", 15 | Required: false, 16 | } 17 | 18 | vrTrace := &cli.BoolFlag{ 19 | Name: "trace", 20 | Usage: "ignored", 21 | Required: false, 22 | } 23 | 24 | vrVCPU := &cli.StringFlag{ 25 | Name: "vcpu", 26 | Usage: "ignored", 27 | Required: false, 28 | } 29 | 30 | vrRAM := &cli.StringFlag{ 31 | Name: "ram", 32 | Usage: "ignored", 33 | Required: false, 34 | } 35 | 36 | return vrConnectionMode, vrTrace, vrVCPU, vrRAM 37 | } 38 | 39 | func packageStartCommands() []*cli.Command { 40 | // vrnetlab compatibility flags are ignored, but exist so containerlab doesn't require any 41 | // changes to work with boxen! 42 | vrConnectionMode, vrTrace, vrVCPU, vrRAM := vrnetlabFlags() 43 | 44 | username, password, hostname := customizationFlags() 45 | 46 | startupConfig := &cli.StringFlag{ 47 | Name: "startup-config", 48 | Usage: "path to startup-config file if desired", 49 | Required: false, 50 | } 51 | 52 | return []*cli.Command{{ 53 | Name: "package-start", 54 | Usage: "start a packaged instance", 55 | Hidden: true, 56 | Flags: []cli.Flag{ 57 | username, 58 | password, 59 | hostname, 60 | vrConnectionMode, 61 | vrTrace, 62 | vrVCPU, 63 | vrRAM, 64 | startupConfig, 65 | }, 66 | Action: func(c *cli.Context) error { 67 | return packageStart( 68 | c.String("username"), 69 | c.String("password"), 70 | c.String("hostname"), 71 | c.String("startup-config"), 72 | ) 73 | }, 74 | }} 75 | } 76 | 77 | func packageStart(username, password, hostname, config string) error { 78 | l, li, err := spinLogger() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig("boxen.yaml")) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return spin(l, li, func() error { return b.PackageStart(username, password, hostname, config) }) 89 | } 90 | -------------------------------------------------------------------------------- /boxen/command/sudo.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | 10 | "golang.org/x/term" 11 | ) 12 | 13 | var ( 14 | sudoerInstance *sudoer //nolint:gochecknoglobals 15 | sudoerOnce sync.Once //nolint:gochecknoglobals 16 | ) 17 | 18 | func newSudoer() *sudoer { 19 | sudoerOnce.Do(func() { 20 | sudoerInstance = &sudoer{} 21 | sudoerInstance.init() 22 | }) 23 | 24 | return sudoerInstance 25 | } 26 | 27 | type sudoer struct { 28 | available bool 29 | passwordless bool 30 | password string 31 | } 32 | 33 | func (s *sudoer) init() { 34 | r, err := Execute("sudo", WithArgs([]string{"-ln"}), WithWait(true)) 35 | if err != nil { 36 | b, _ := r.ReadStderr() 37 | 38 | if bytes.Contains(b, []byte("password is required")) { 39 | s.available = true 40 | 41 | return 42 | } 43 | 44 | return 45 | } 46 | 47 | b, _ := r.ReadStdout() 48 | 49 | if bytes.Contains(b, []byte("command not found")) { 50 | return 51 | } 52 | 53 | s.available = true 54 | 55 | // user is a passwordless user *or* user is root (indicating user ran boxen w/ sudo) 56 | if bytes.Contains(b, []byte("(ALL) NOPASSWD: ALL")) || 57 | bytes.Contains(b, []byte("(ALL : ALL) NOPASSWD: ALL")) || 58 | bytes.Contains(b, []byte("root may run the following commands")) { 59 | s.passwordless = true 60 | } 61 | } 62 | 63 | func (s *sudoer) getSudoPassword() { 64 | fmt.Printf( 65 | "privilege escalation required and user is not passwordless sudoer, please enter sudo password: ", 66 | ) 67 | 68 | userInput, err := term.ReadPassword(int(os.Stdin.Fd())) 69 | if err != nil { 70 | panic("something went wrong getting password from user, cannot continue") 71 | } 72 | 73 | s.password = string(userInput) 74 | } 75 | 76 | func (s *sudoer) updateCmd(cmd string, args []string) (string, []string) { //nolint:gocritic 77 | if !s.available { 78 | return cmd, args 79 | } 80 | 81 | if s.passwordless { 82 | args = append([]string{cmd}, args...) 83 | 84 | return "sudo", args 85 | } 86 | 87 | if s.password == "" { 88 | s.getSudoPassword() 89 | } 90 | 91 | args = []string{ 92 | "-c", 93 | fmt.Sprintf("echo '%s' | sudo -S %s %s", s.password, cmd, strings.Join(args, " ")), 94 | } 95 | 96 | return "sh", args 97 | } 98 | -------------------------------------------------------------------------------- /boxen/boxen/mgmtnet_test.go: -------------------------------------------------------------------------------- 1 | package boxen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/carlmontanari/boxen/boxen/boxen" 7 | "github.com/carlmontanari/boxen/boxen/config" 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestZipPlatformProfileNats(t *testing.T) { 12 | tests := []struct { 13 | desc string 14 | tcp []int 15 | udp []int 16 | wantTCPNats []*config.NatPortPair 17 | wantUDPNats []*config.NatPortPair 18 | }{ 19 | { 20 | desc: "two tcp ports and one udp", 21 | tcp: []int{22, 23}, 22 | udp: []int{161}, 23 | wantTCPNats: []*config.NatPortPair{ 24 | { 25 | InstanceSide: 22, 26 | HostSide: 48000, 27 | }, 28 | { 29 | InstanceSide: 23, 30 | HostSide: 48001, 31 | }, 32 | }, 33 | wantUDPNats: []*config.NatPortPair{ 34 | { 35 | InstanceSide: 161, 36 | HostSide: 48002, 37 | }, 38 | }, 39 | }, 40 | { 41 | desc: "two tcp ports and no udp", 42 | tcp: []int{22, 23}, 43 | wantTCPNats: []*config.NatPortPair{ 44 | { 45 | InstanceSide: 22, 46 | HostSide: 48000, 47 | }, 48 | { 49 | InstanceSide: 23, 50 | HostSide: 48001, 51 | }, 52 | }, 53 | wantUDPNats: []*config.NatPortPair{}, 54 | }, 55 | { 56 | desc: "no tcp ports and two udp", 57 | udp: []int{161, 200}, 58 | wantTCPNats: []*config.NatPortPair{}, 59 | wantUDPNats: []*config.NatPortPair{ 60 | { 61 | InstanceSide: 161, 62 | HostSide: 48000, 63 | }, 64 | { 65 | InstanceSide: 200, 66 | HostSide: 48001, 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.desc, func(t *testing.T) { 74 | tcp, udp := boxen.ZipPlatformProfileNats(tt.tcp, tt.udp) 75 | 76 | if !cmp.Equal(tcp, tt.wantTCPNats) { 77 | t.Fatalf( 78 | "%s: actual and expected inputs do not match\nactual: %+v\nexpected:%+v", 79 | tt.desc, 80 | tcp, 81 | tt.wantTCPNats, 82 | ) 83 | } 84 | 85 | if !cmp.Equal(udp, tt.wantUDPNats) { 86 | t.Fatalf( 87 | "%s: actual and expected inputs do not match\nactual: %+v\nexpected:%+v", 88 | tt.desc, 89 | udp, 90 | tt.wantUDPNats, 91 | ) 92 | } 93 | }, 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /boxen/boxen/boxen.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | "github.com/carlmontanari/boxen/boxen/config" 8 | "github.com/carlmontanari/boxen/boxen/logging" 9 | "github.com/carlmontanari/boxen/boxen/platforms" 10 | "github.com/carlmontanari/boxen/boxen/util" 11 | ) 12 | 13 | // args is a simple struct for holding Boxen option arguments. 14 | type args struct { 15 | logger *logging.Instance 16 | config string 17 | } 18 | 19 | // Boxen is the main "manager"/instance containing all the configuration data and maps of instances. 20 | type Boxen struct { 21 | ConfigPath string 22 | Config *config.Config 23 | configLock *sync.Mutex 24 | Instances map[string]platforms.Platform 25 | instancesLock *sync.Mutex 26 | Logger *logging.Instance 27 | } 28 | 29 | // NewBoxen returns an instance of Boxen with any provided Option applied. 30 | func NewBoxen(opts ...Option) (*Boxen, error) { 31 | b := &Boxen{ 32 | Instances: map[string]platforms.Platform{}, 33 | instancesLock: &sync.Mutex{}, 34 | configLock: &sync.Mutex{}, 35 | } 36 | 37 | a := &args{} 38 | 39 | for _, o := range opts { 40 | err := o(a) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | } 46 | 47 | if a.logger != nil { 48 | b.Logger = a.logger 49 | } else { 50 | var err error 51 | 52 | b.Logger, err = logging.NewInstance(log.Print) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | } 58 | 59 | if a.config != "" { 60 | cp, err := util.ResolveFile(a.config) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | b.ConfigPath = cp 66 | 67 | cfg, err := config.NewConfigFromFile(b.ConfigPath) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | b.Config = cfg 73 | } 74 | 75 | return b, nil 76 | } 77 | 78 | // modifyInstanceMap is a simple method accepting a function f to write to the Instances map behind 79 | // a simple sync.Mutex lock. This method is necessary due to the start and stop operations spawning 80 | // goroutines for each instance provided by the user. Realistically this would *probably* never be 81 | // a problem, but running things with -race flag definitely indicated potential issues so here we 82 | // are! 83 | func (b *Boxen) modifyInstanceMap(f func()) { 84 | b.instancesLock.Lock() 85 | defer b.instancesLock.Unlock() 86 | 87 | f() 88 | } 89 | -------------------------------------------------------------------------------- /boxen/instance/qemuhealth.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | ) 13 | 14 | const ( 15 | HealthOK = 200 16 | HealthBad = 500 17 | 18 | watchCount = 12 19 | watchSleep = 5 20 | ) 21 | 22 | // WatchMainProc watches until the process is running, and then emits an error on c if the process 23 | // stops running. If it receives anything on the stop channel it exits. This can be used during 24 | // long-running operations such as "Install" to make sure that if the qemu proc dies we exit 25 | // immediately rather than sit around hoping things happen on the console :). 26 | func (i *Qemu) WatchMainProc(c chan error, stop chan bool) { 27 | started := false 28 | checkCount := 0 29 | 30 | for { 31 | select { 32 | case <-stop: 33 | return 34 | default: 35 | if !started && checkCount == watchCount { 36 | c <- fmt.Errorf( 37 | "%w: process not tagged as started, probably exited very quickly, "+ 38 | "something is almost certainly wrong with the qemu launch command", 39 | util.ErrInspectionError, 40 | ) 41 | } 42 | 43 | r := i.validatePid() 44 | 45 | if !started && r { 46 | started = true 47 | } 48 | 49 | if started && !r { 50 | c <- fmt.Errorf("%w: process already exited", util.ErrInstanceError) 51 | } 52 | 53 | checkCount++ 54 | 55 | time.Sleep(watchSleep * time.Second) 56 | } 57 | } 58 | } 59 | 60 | func (i *Qemu) waitMainProc() { 61 | _ = i.Proc.Wait() 62 | 63 | i.Proc = nil 64 | i.PID = 0 65 | } 66 | 67 | func (i *Qemu) healthEndpoint(w http.ResponseWriter, r *http.Request) { 68 | _ = r 69 | 70 | if i.Proc != nil && i.PID > 0 { 71 | w.WriteHeader(HealthOK) 72 | 73 | return 74 | } 75 | 76 | w.WriteHeader(HealthBad) 77 | } 78 | 79 | func (i *Qemu) healthServer() { 80 | http.HandleFunc("/", i.healthEndpoint) 81 | 82 | _ = http.ListenAndServe(":7777", nil) 83 | } 84 | 85 | func (i *Qemu) RunUntilSigInt() { 86 | go i.healthServer() 87 | go i.waitMainProc() 88 | 89 | sigs := make(chan os.Signal, 1) 90 | done := make(chan bool, 1) 91 | 92 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 93 | 94 | go func() { 95 | <-sigs 96 | done <- true 97 | }() 98 | <-done 99 | } 100 | -------------------------------------------------------------------------------- /boxen/util/files.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/carlmontanari/boxen/boxen" 13 | ) 14 | 15 | // ExpandPath expands user home path in provided path p. 16 | func ExpandPath(p string) string { 17 | userPath, _ := os.UserHomeDir() 18 | 19 | p = strings.Replace(p, "~", userPath, 1) 20 | 21 | return p 22 | } 23 | 24 | // DirectoryExists checks if a given path exists (and is a directory). 25 | func DirectoryExists(d string) bool { 26 | info, err := os.Stat(d) 27 | if os.IsNotExist(err) { 28 | return false 29 | } 30 | 31 | return info.IsDir() 32 | } 33 | 34 | // FileExists checks if a given file exists (and is not a directory). 35 | func FileExists(f string) bool { 36 | info, err := os.Stat(f) 37 | if os.IsNotExist(err) { 38 | return false 39 | } 40 | 41 | return !info.IsDir() 42 | } 43 | 44 | // ResolveFile resolves provided file path. 45 | func ResolveFile(f string) (string, error) { 46 | expanded := ExpandPath(f) 47 | 48 | if FileExists(expanded) { 49 | return filepath.Abs(expanded) 50 | } 51 | 52 | return "", fmt.Errorf("%w: failed resolving file '%s'", ErrInspectionError, f) 53 | } 54 | 55 | // CopyFile copies a file source `s` to destination `d`. 56 | func CopyFile(s, d string) error { 57 | srcPath, err := ResolveFile(s) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | src, err := os.Open(srcPath) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | defer src.Close() 68 | 69 | dest, err := os.Create(d) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | _, err = io.Copy(dest, src) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = dest.Sync() 80 | 81 | return err 82 | } 83 | 84 | // CopyAsset copies an asset file source `s` to destination `d`. 85 | func CopyAsset(s, d string) error { 86 | sFile, err := boxen.Assets.Open(fmt.Sprintf("assets/%s", s)) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | sReader := bufio.NewReader(sFile) 92 | 93 | dest, err := os.Create(d) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | _, err = io.Copy(dest, sReader) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = dest.Sync() 104 | 105 | return err 106 | } 107 | 108 | // CommandExists checks if a command `cmd` exists in the PATH. 109 | func CommandExists(cmd string) bool { 110 | _, err := exec.LookPath(cmd) 111 | return err == nil 112 | } 113 | -------------------------------------------------------------------------------- /boxen/docker/options.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "io" 4 | 5 | // Option sets a docker args option. 6 | type Option func(*args) error 7 | 8 | // WithWorkDir sets the working directory argument for docker operations. 9 | func WithWorkDir(workDir string) Option { 10 | return func(o *args) error { 11 | o.workDir = workDir 12 | 13 | return nil 14 | } 15 | } 16 | 17 | // WithDockerfile sets '-f' dockerfile argument to provided name f for docker operations. 18 | func WithDockerfile(f string) Option { 19 | return func(o *args) error { 20 | o.dockerfile = f 21 | 22 | return nil 23 | } 24 | } 25 | 26 | // WithCidFile sets the cidfile argument to the provided name f for docker operations. 27 | func WithCidFile(f string) Option { 28 | return func(o *args) error { 29 | o.cidFile = f 30 | 31 | return nil 32 | } 33 | } 34 | 35 | // WithPrivileged allows for running containers with the privileged flag set. 36 | func WithPrivileged(b bool) Option { 37 | return func(o *args) error { 38 | o.privileged = b 39 | 40 | return nil 41 | } 42 | } 43 | 44 | // WithRepo sets the container repository value for docker operations. 45 | func WithRepo(s string) Option { 46 | return func(o *args) error { 47 | o.repo = s 48 | 49 | return nil 50 | } 51 | } 52 | 53 | // WithTag sets the container tag value for docker operations. 54 | func WithTag(s string) Option { 55 | return func(o *args) error { 56 | o.tag = s 57 | 58 | return nil 59 | } 60 | } 61 | 62 | // WithContainer sets the container id value for docker operations. 63 | func WithContainer(s string) Option { 64 | return func(o *args) error { 65 | o.container = s 66 | 67 | return nil 68 | } 69 | } 70 | 71 | // WithCommitChange sets the docker args commit changes value to the provided string s. 72 | func WithCommitChange(s string) Option { 73 | return func(o *args) error { 74 | o.commitChange = s 75 | 76 | return nil 77 | } 78 | } 79 | 80 | // WithStdOut sets the docker args stdout flag to the provided io.Writer. 81 | func WithStdOut(stdout io.Writer) Option { 82 | return func(o *args) error { 83 | o.stdOut = stdout 84 | 85 | return nil 86 | } 87 | } 88 | 89 | // WithStdErr sets the docker args stderr flag to the provided io.Writer. 90 | func WithStdErr(stderr io.Writer) Option { 91 | return func(o *args) error { 92 | o.stdErr = stderr 93 | 94 | return nil 95 | } 96 | } 97 | 98 | // WithNoCache sets the docker args flag to disable cache layers for building container images. 99 | func WithNoCache(c bool) Option { 100 | return func(o *args) error { 101 | o.nocache = c 102 | 103 | return nil 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /boxen/cli/provision.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/carlmontanari/boxen/boxen/boxen" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func provisionCommands() []*cli.Command { 13 | config := boxenGlobalFlags() 14 | 15 | instances := &cli.StringFlag{ 16 | Name: "instances", 17 | Usage: "instance or comma sep string of instances to provision", 18 | Required: false, 19 | } 20 | 21 | vendor := &cli.StringFlag{ 22 | Name: "vendor", 23 | Usage: "name of the vendor (ex: 'arista') for the instance(s) to provision", 24 | Required: true, 25 | } 26 | 27 | platform := &cli.StringFlag{ 28 | Name: "platform", 29 | Usage: "name of the platform (ex: 'veos') for the instance(s) to provision", 30 | Required: true, 31 | } 32 | 33 | source := &cli.StringFlag{ 34 | Name: "source-disk", 35 | Usage: "installed source disk to use for provisioning the instance(s)", 36 | Required: false, 37 | } 38 | 39 | profile := &cli.StringFlag{ 40 | Name: "profile", 41 | Usage: "hardware profile to apply to the instance(s)", 42 | Required: false, 43 | } 44 | 45 | return []*cli.Command{ 46 | { 47 | Name: "provision", 48 | Usage: "provision a local boxen instance", 49 | Flags: []cli.Flag{ 50 | config, 51 | instances, 52 | vendor, 53 | platform, 54 | source, 55 | profile, 56 | }, 57 | Action: func(c *cli.Context) error { 58 | return Provision( 59 | c.String("config"), 60 | c.String("instances"), 61 | c.String("vendor"), 62 | c.String("platform"), 63 | c.String("sourceDisk"), 64 | c.String("profile"), 65 | ) 66 | }, 67 | }, 68 | } 69 | } 70 | 71 | func Provision(config, instances, vendor, platform, sourceDisk, profile string) error { 72 | l, li, err := spinLogger() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | b, err := boxen.NewBoxen(boxen.WithLogger(li), boxen.WithConfig(config)) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return spin( 83 | l, 84 | li, 85 | func() error { 86 | wg := &sync.WaitGroup{} 87 | 88 | instanceSlice := strings.Split(instances, ",") 89 | 90 | var errs []error 91 | 92 | for _, instance := range instanceSlice { 93 | wg.Add(1) 94 | 95 | i := instance 96 | 97 | go func() { 98 | err = b.Provision(i, vendor, platform, sourceDisk, profile) 99 | 100 | if err != nil { 101 | errs = append(errs, err) 102 | } 103 | 104 | wg.Done() 105 | }() 106 | } 107 | 108 | wg.Wait() 109 | 110 | if len(errs) > 0 { 111 | return errs[0] 112 | } 113 | 114 | return nil 115 | }, 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /boxen/config/allocate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // AllocatedInstanceIDs returns a slice of integers of all currently allocated instance IDs in the 4 | // local boxen config. 5 | func (c *Config) AllocatedInstanceIDs() []int { 6 | allocatedIDs := make([]int, 0) 7 | 8 | for _, data := range c.Instances { 9 | allocatedIDs = append(allocatedIDs, data.ID) 10 | } 11 | 12 | return allocatedIDs 13 | } 14 | 15 | // AllocatedMonitorPorts returns a slice of integers of all currently allocated monitor port IDs in 16 | // the local boxen config. 17 | func (c *Config) AllocatedMonitorPorts() []int { 18 | allocatedMonitorPorts := make([]int, 0) 19 | 20 | for _, data := range c.Instances { 21 | allocatedMonitorPorts = append(allocatedMonitorPorts, data.Hardware.MonitorPort) 22 | } 23 | 24 | return allocatedMonitorPorts 25 | } 26 | 27 | // AllocatedSerialPorts returns a slice of integers of all currently allocated serial port IDs in 28 | // the local boxen config. 29 | func (c *Config) AllocatedSerialPorts() []int { 30 | allocatedSerialPorts := make([]int, 0) 31 | 32 | for _, data := range c.Instances { 33 | allocatedSerialPorts = append(allocatedSerialPorts, data.Hardware.SerialPorts...) 34 | } 35 | 36 | return allocatedSerialPorts 37 | } 38 | 39 | // AllocatedHostSideNatPorts returns a slice of integers of all currently allocated "host side" nat 40 | // ports in the local boxen config. These ports are the ephemeral range ports that get applied to 41 | // the qemu hostfwd nat directives for the local virtual machines. 42 | func (c *Config) AllocatedHostSideNatPorts() []int { 43 | allocatedNatPorts := make([]int, 0) 44 | 45 | for _, data := range c.Instances { 46 | if data.MgmtIntf != nil && data.MgmtIntf.Nat != nil { 47 | for _, nat := range data.MgmtIntf.Nat.TCP { 48 | allocatedNatPorts = append(allocatedNatPorts, nat.HostSide) 49 | } 50 | 51 | for _, nat := range data.MgmtIntf.Nat.UDP { 52 | allocatedNatPorts = append(allocatedNatPorts, nat.HostSide) 53 | } 54 | } 55 | } 56 | 57 | return allocatedNatPorts 58 | } 59 | 60 | // AllocatedDataPlaneListenPorts returns a slice of integers of all currently allocated "listen" 61 | // ports in the local boxen config. These ports are the ephemeral range ports that get applied to 62 | // the qemu udp listen ports for the "dataplane" ports of the virtual machines. 63 | func (c *Config) AllocatedDataPlaneListenPorts() []int { 64 | allocatedListenPorts := make([]int, 0) 65 | 66 | for _, data := range c.Instances { 67 | if data.DataPlaneIntf != nil && data.DataPlaneIntf.SocketConnectMap != nil { 68 | for _, pair := range data.DataPlaneIntf.SocketConnectMap { 69 | allocatedListenPorts = append(allocatedListenPorts, pair.Listen) 70 | } 71 | } 72 | } 73 | 74 | return allocatedListenPorts 75 | } 76 | -------------------------------------------------------------------------------- /boxen/platforms/util.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/carlmontanari/boxen/boxen/util" 8 | ) 9 | 10 | func diskToVendorPlatformMap() map[*regexp.Regexp][]string { 11 | return map[*regexp.Regexp][]string{ 12 | regexp.MustCompile(`csr1000v-.*.qcow2`): { 13 | "cisco", 14 | "csr1000v", 15 | }, 16 | regexp.MustCompile(`(?i)xrv9k-fullk9-x.*.qcow2`): { 17 | "cisco", 18 | "xrv9k"}, 19 | regexp.MustCompile(`(?i)(nexus9300v(?:64)?|nxosv).*.qcow2`): { 20 | "cisco", 21 | "n9kv"}, 22 | regexp.MustCompile(`(?i)vEOS-lab-.*.vmdk`): { 23 | "arista", 24 | "veos"}, 25 | regexp.MustCompile(`(?i)(junos-media-vsrx-x86-64|media-vsrx)-vmdisk.*.qcow2`): { 26 | "juniper", 27 | "vsrx"}, 28 | regexp.MustCompile(`(?i)PA-VM-KVM.*.qcow2`): { 29 | "paloalto", 30 | "panos", 31 | }, 32 | regexp.MustCompile(`(?i)check_point_r.*.cloudguard.*.qcow2`): { 33 | "checkpoint", 34 | "cloudguard", 35 | }, 36 | } 37 | } 38 | 39 | func GetPlatformTypeFromDisk(f string) (vendor, platform string, err error) { 40 | dToPtMap := diskToVendorPlatformMap() 41 | 42 | for pattern, pT := range dToPtMap { 43 | if pattern.MatchString(f) { 44 | // target disk matches this vendor/platform pair 45 | return pT[0], pT[1], nil 46 | } 47 | } 48 | 49 | return "", "", fmt.Errorf( 50 | "%w: cannot resolve target platform type from provided disk", 51 | util.ErrInspectionError, 52 | ) 53 | } 54 | 55 | func pTDiskToVersionMap() map[string]*regexp.Regexp { 56 | return map[string]*regexp.Regexp{ 57 | PlatformTypeAristaVeos: regexp.MustCompile(`(?i)(\d+\.\d+\.[a-z0-9\-]+(\.\d+[a-z]?)?)`), 58 | PlatformTypeCiscoCsr1000v: regexp.MustCompile(`(?i)(?:universalk9.*?)(\d+\.\d+\.\d+)`), 59 | PlatformTypeCiscoXrv9k: regexp.MustCompile( 60 | `(?i)(?:xrv9k-fullk9-x\.vrr-)(\d+\.\d+\.\d+)`, 61 | ), 62 | PlatformTypeCiscoN9kv: regexp.MustCompile( 63 | `(?i)(?:(?:nexus9300v(?:64)?|nxosv(?:-final)?)\.)(\d+\.\d+\.\d+)`, 64 | ), 65 | PlatformTypeJuniperVsrx: regexp.MustCompile( 66 | `(?i)(?:junos-media-vsrx-x86-64-vmdisk-|media-vsrx-vmdisk-)(\d+\.[\w-]+\.\d+).qcow2`, 67 | ), 68 | PlatformTypePaloAltoPanos: regexp.MustCompile( 69 | `(?i)(?:pa-vm-kvm-)(\d+\.\d+\.\d+(?:-h\d+)?).qcow2`), 70 | PlatformTypeCheckpointCloudguard: regexp.MustCompile( 71 | `(?i)check_point_(r\d+\.\d+)_cloudguard_.*.qcow2`), 72 | } 73 | } 74 | 75 | func GetDiskVersion(f, pT string) (string, error) { 76 | targetVersionMap := pTDiskToVersionMap() 77 | 78 | pattern := targetVersionMap[pT] 79 | 80 | diskVersionMatches := pattern.FindStringSubmatch(f) 81 | 82 | if len(diskVersionMatches) == 0 { 83 | return "", fmt.Errorf( 84 | "%w: cannot determine version from provided disk", 85 | util.ErrInspectionError, 86 | ) 87 | } 88 | 89 | return diskVersionMatches[1], nil 90 | } 91 | -------------------------------------------------------------------------------- /boxen/util/spinner.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type Spinner struct { 11 | mu *sync.RWMutex 12 | Delay time.Duration 13 | chars []string 14 | Prefix string 15 | Suffix string 16 | active bool 17 | stopChan chan struct{} 18 | PostUpdate func(s *Spinner) 19 | Finale func(s *Spinner) string 20 | } 21 | 22 | func NewSpinner() *Spinner { 23 | s := &Spinner{ 24 | Delay: 250 * time.Millisecond, 25 | chars: []string{ 26 | "⠈", 27 | "⠉", 28 | "⠋", 29 | "⠓", 30 | "⠒", 31 | "⠐", 32 | "⠐", 33 | "⠒", 34 | "⠖", 35 | "⠦", 36 | "⠤", 37 | "⠠", 38 | "⠠", 39 | "⠤", 40 | "⠦", 41 | "⠖", 42 | "⠒", 43 | "⠐", 44 | "⠐", 45 | "⠒", 46 | "⠓", 47 | "⠋", 48 | "⠉", 49 | "⠈", 50 | }, 51 | mu: &sync.RWMutex{}, 52 | stopChan: make(chan struct{}, 1), 53 | } 54 | 55 | return s 56 | } 57 | 58 | func (s *Spinner) resetPrompt() { 59 | // move cursor to column 1 and then erase to end of the line -- this clears our previously 60 | // written spinner so, we can output an updated one. handy link: 61 | // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 62 | _, _ = fmt.Fprintf(os.Stdout, "\x1b[1G") 63 | _, _ = fmt.Fprintf(os.Stdout, "\x1b[0K") 64 | } 65 | 66 | func (s *Spinner) Start() { 67 | s.mu.Lock() 68 | if s.active { 69 | s.mu.Unlock() 70 | return 71 | } 72 | 73 | // hides the cursor 74 | _, _ = fmt.Fprint(os.Stdout, "\033[?25l") 75 | 76 | s.active = true 77 | s.mu.Unlock() 78 | 79 | go func() { 80 | for { 81 | for i := 0; i < len(s.chars); i++ { 82 | select { 83 | case <-s.stopChan: 84 | return 85 | default: 86 | s.mu.Lock() 87 | if !s.active { 88 | s.mu.Unlock() 89 | return 90 | } 91 | 92 | s.resetPrompt() 93 | 94 | out := fmt.Sprintf("\r%s\n\t%s%s ", s.Prefix, s.chars[i], s.Suffix) 95 | if s.Prefix == "" { 96 | out = fmt.Sprintf("\r%s\t%s%s ", s.Prefix, s.chars[i], s.Suffix) 97 | } 98 | 99 | _, _ = fmt.Fprint( 100 | os.Stdout, 101 | out, 102 | ) 103 | 104 | if s.PostUpdate != nil { 105 | s.PostUpdate(s) 106 | } 107 | 108 | s.mu.Unlock() 109 | time.Sleep(s.Delay) 110 | } 111 | } 112 | } 113 | }() 114 | } 115 | 116 | func (s *Spinner) Stop() { 117 | s.mu.Lock() 118 | 119 | defer s.mu.Unlock() 120 | 121 | if s.active { 122 | s.active = false 123 | 124 | if s.Finale != nil { 125 | _, _ = fmt.Fprint( 126 | os.Stdout, 127 | s.Finale(s), 128 | ) 129 | } 130 | 131 | // makes the cursor visible again 132 | _, _ = fmt.Fprint(os.Stdout, "\033[?25h") 133 | fmt.Println() 134 | 135 | s.stopChan <- struct{}{} 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /boxen/boxen/packageinstall.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/carlmontanari/boxen/boxen/command" 7 | "github.com/carlmontanari/boxen/boxen/instance" 8 | "github.com/carlmontanari/boxen/boxen/platforms" 9 | "github.com/carlmontanari/boxen/boxen/util" 10 | ) 11 | 12 | func (b *Boxen) getPackagedInstanceName() string { 13 | var name string 14 | 15 | for n := range b.Config.Instances { 16 | // there will only ever be one instance in the "package" mode, so we'll just iterate, get 17 | // that one instance's name and peace out; probably a smarter way... 18 | name = n 19 | break 20 | } 21 | 22 | return name 23 | } 24 | 25 | func (b *Boxen) shrinkDisk(name string) error { 26 | err := os.Rename(b.Config.Instances[name].Disk, "fat.qcow2") 27 | if err != nil { 28 | return err 29 | } 30 | 31 | _, err = command.Execute( 32 | "virt-sparsify", 33 | command.WithArgs([]string{"fat.qcow2", "--compress", b.Config.Instances[name].Disk}), 34 | command.WithWait(true), 35 | ) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | err = os.Remove("fat.qcow2") 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = os.RemoveAll("./var/tmp/.guestfs-0") 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // PackageInstall is the function that is run *in* the initially built container image. This 54 | // function handles the initial provisioning of the instance. 55 | func (b *Boxen) PackageInstall() error { 56 | b.Logger.Debug("package install starting") 57 | 58 | name := b.getPackagedInstanceName() 59 | 60 | // for things *not* packaging we will want to merge the instance config w/ the profile and/or 61 | // defaults, that is not necessary for packaging since we just load up the config all on the 62 | // instance though! 63 | q, err := platforms.NewPlatformFromConfig( 64 | name, 65 | b.Config, 66 | &instance.Loggers{ 67 | Base: b.Logger, 68 | Stdout: os.Stdout, 69 | Stderr: os.Stdout, 70 | Console: os.Stdout, 71 | }, 72 | ) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | b.Instances[name] = q 78 | 79 | configLines, err := b.RenderInitialConfig(name) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | b.Logger.Debug("begin instance install") 85 | 86 | err = b.Instances[name].Install( 87 | platforms.WithInstallConfig(configLines), 88 | instance.WithSudo(false), 89 | ) 90 | if err != nil { 91 | b.Logger.Criticalf("package installation failed: %s\n", err) 92 | 93 | return err 94 | } 95 | 96 | if util.GetEnvIntOrDefault("BOXEN_SPARSIFY_DISK", 0) > 0 { 97 | err = b.shrinkDisk(name) 98 | if err != nil { 99 | b.Logger.Criticalf("shrinking installation disk failed: %s\n", err) 100 | 101 | return err 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /boxen/platforms/base.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import "github.com/carlmontanari/boxen/boxen/instance" 4 | 5 | type Platform interface { 6 | // Base embeds Install, Start, Stop and RunUntilSigInt methods that should be common for any 7 | // Platform. Platforms may (probably) will need to override Install and Start in order to pass 8 | // the appropriate options to modify launch commands and the like. 9 | instance.Base 10 | 11 | // Package builds any necessary files for instance installation/start such as a config file or 12 | // a similar and checks that the srcDir contains any files that must be included for the 13 | // instance such as bios files or bootloaders etc. The `pkgFiles` are files that should be 14 | // included during initial packaging (of either the initial build container, or the local temp 15 | // directory in the case of local VMs). The `runFiles` are files that are required to actually 16 | // run the device, and therefore should be collocated with the final disk -- since all files get 17 | // copied to the container in the case of a "package" build, this is specific to local VM mode. 18 | Package(srcDir, pkgDir string) (pkgFiles []string, runFiles []string, err error) 19 | 20 | // Config sends some lines of configs to the device -- *probably* via console, but up to the 21 | // platform how that happens. Hopefully this will be satisfied by ScrapliConsole being embedded 22 | // in most platform cases. 23 | Config(lines []string) error 24 | // InstallConfig "installs" a (usually) startup config on the device. This is once again up to 25 | // the platform how this is implemented, but for "core" platforms this will be done via scrapli 26 | // "cfg" functionality to handle config *replaces* or, optionally, merge operations. The config 27 | // to be installed is whatever is in the file path `f`. 28 | InstallConfig(f string, replace bool) error 29 | 30 | // Detach closes any connections to the instance -- *probably* this means it closes the console 31 | // connection but the base qemu instance doesn't need to know/care if its console or something 32 | // else entirely. Hopefully this will be satisfied by ScrapliConsole being embedded in most 33 | // platform cases. 34 | Detach() error 35 | 36 | // SaveConfig saves the config of the instance. Platforms *should* implement some kind of check 37 | // and/or backoff that ensures that configs are able to be saved -- meaning that some devices do 38 | // not allow for configurations to be saved immediately after startup -- this method *should* 39 | // handle that and sleep for some duration before trying again! 40 | SaveConfig() error 41 | 42 | // SetUserPass sets the username/password -- used for "package start" mode. 43 | SetUserPass(usr, pwd string) error 44 | // SetHostname sets the hostname -- used for "package start" mode. 45 | SetHostname(h string) error 46 | 47 | // GetPid returns the instances pid or -1. 48 | GetPid() int 49 | } 50 | -------------------------------------------------------------------------------- /boxen/command/result.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/carlmontanari/boxen/boxen/util" 11 | ) 12 | 13 | const ( 14 | checkErrDuration = 10 15 | checkErrInterval = 250 16 | ) 17 | 18 | type Result struct { 19 | Proc *exec.Cmd 20 | Stdin io.WriteCloser 21 | // stdout and stderr are internal buffers of the stdout/stderr data. Users can of course provide 22 | // additional io.Writer implementations which will be added to the io.MultiWriter that gets set 23 | // as the stdout/stderr writers. 24 | stdout *util.LockingWriterReader 25 | stderr *util.LockingWriterReader 26 | stderrInt *util.LockingWriterReader 27 | } 28 | 29 | type checkArgs struct { 30 | ignore [][]byte 31 | isError [][]byte 32 | duration time.Duration 33 | interval time.Duration 34 | } 35 | 36 | func (r *Result) setIO(a *args) error { 37 | // set the stderr/stderrInt/stdout *before* trying to set stdin -- if stdin fails we need 38 | // the stderr/stderrInt/stdout set for subsequent CheckStdErr/ReadStdout/ReadStderr methods to 39 | // not fail 40 | ow := []io.Writer{r.stdout} 41 | 42 | if a.stdOut != nil { 43 | ow = append(ow, a.stdOut) 44 | } 45 | 46 | r.Proc.Stdout = io.MultiWriter(ow...) 47 | 48 | ew := []io.Writer{r.stderr, r.stderrInt} 49 | 50 | if a.stdErr != nil { 51 | ew = append(ew, a.stdErr) 52 | } 53 | 54 | r.Proc.Stderr = io.MultiWriter(ew...) 55 | 56 | stdin, err := r.Proc.StdinPipe() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | r.Stdin = stdin 62 | 63 | return nil 64 | } 65 | 66 | // ReadStdout returns whatever standard out is currently in the internal stdout buffer. 67 | func (r *Result) ReadStdout() ([]byte, error) { 68 | return r.stdout.Read() 69 | } 70 | 71 | // ReadStderr returns whatever standard out is currently in the internal stderr buffer. 72 | func (r *Result) ReadStderr() ([]byte, error) { 73 | return r.stderrInt.Read() 74 | } 75 | 76 | // CheckStdErr blocks while reading stderr output from a process. CheckOptions define what is safe 77 | // to ignore or what constitutes an error. You can modify the check duration and sleep interval of 78 | // the check with CheckOption settings. 79 | func (r *Result) CheckStdErr(opts ...CheckOption) error { 80 | if r.Proc.ProcessState != nil && r.Proc.ProcessState.Exited() { 81 | return fmt.Errorf("%w: cannot check stsderr, process already exited", util.ErrCommandError) 82 | } 83 | 84 | a := &checkArgs{ 85 | duration: checkErrDuration * time.Second, 86 | interval: checkErrInterval * time.Millisecond, 87 | } 88 | 89 | for _, o := range opts { 90 | err := o(a) 91 | 92 | if err != nil { 93 | return err 94 | } 95 | } 96 | 97 | c := make(chan error, 1) 98 | 99 | go func() { 100 | for { 101 | b, err := r.stderrInt.Read() 102 | 103 | if err != nil && !errors.Is(err, io.EOF) { 104 | c <- err 105 | 106 | return 107 | } 108 | 109 | if a.isError != nil && util.ByteSliceContains(a.isError, b) { 110 | c <- fmt.Errorf("%w: stderr contains output explicitly marked as bad", util.ErrCommandError) 111 | 112 | return 113 | } 114 | 115 | if a.ignore == nil && !util.ByteSliceAllNull(b) { 116 | c <- fmt.Errorf("%w: stderr contains output but it shouldn't", util.ErrCommandError) 117 | 118 | return 119 | } 120 | 121 | time.Sleep(a.interval) 122 | } 123 | }() 124 | 125 | select { 126 | case err := <-c: 127 | return err 128 | case <-time.After(a.duration): 129 | return nil 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /boxen/boxen/allocate.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/config" 7 | "github.com/carlmontanari/boxen/boxen/util" 8 | ) 9 | 10 | func (b *Boxen) allocateInstanceID() (int, error) { 11 | allocatedIDs := b.Config.AllocatedInstanceIDs() 12 | 13 | for i := 1; i <= config.MAXINSTANCES; i++ { 14 | if !util.IntSliceContains(allocatedIDs, i) { 15 | return i, nil 16 | } 17 | } 18 | 19 | return -1, fmt.Errorf("%w: unable to allocate instance ID", util.ErrAllocationError) 20 | } 21 | 22 | func (b *Boxen) allocateInstanceIDReverse() (int, error) { 23 | allocatedIDs := b.Config.AllocatedInstanceIDs() 24 | 25 | for i := config.MAXINSTANCES; i >= 1; i-- { 26 | if !util.IntSliceContains(allocatedIDs, i) { 27 | return i, nil 28 | } 29 | } 30 | 31 | return -1, fmt.Errorf("%w: unable to allocate instance ID", util.ErrAllocationError) 32 | } 33 | 34 | func (b *Boxen) allocateMonitorPort(instanceID int) int { 35 | return instanceID + config.MONITORPORTBASE 36 | } 37 | 38 | func (b *Boxen) allocateSerialPorts(numRequired, instanceID int) ([]int, error) { 39 | if numRequired <= 0 { 40 | return nil, nil 41 | } 42 | 43 | allocatedSerialPorts := []int{instanceID + config.SERIALPORTBASE} 44 | 45 | if numRequired == 1 { 46 | return allocatedSerialPorts, nil 47 | } 48 | 49 | existingSerialPorts := b.Config.AllocatedSerialPorts() 50 | 51 | for i := config.SERIALPORTHI; i >= config.SERIALPORTLOW; i-- { 52 | if !util.IntSliceContains(existingSerialPorts, i) { 53 | allocatedSerialPorts = append(allocatedSerialPorts, i) 54 | } 55 | 56 | if len(allocatedSerialPorts) == numRequired { 57 | return allocatedSerialPorts, nil 58 | } 59 | } 60 | 61 | return nil, fmt.Errorf("%w: unable to allocate serial port(s)", util.ErrAllocationError) 62 | } 63 | 64 | func (b *Boxen) allocateMgmtNatPorts( 65 | natPorts []int, 66 | pendingNatPorts []*config.NatPortPair, 67 | ) ([]*config.NatPortPair, error) { 68 | natPortPairs := make([]*config.NatPortPair, 0) 69 | 70 | existingNatPorts := b.Config.AllocatedHostSideNatPorts() 71 | 72 | for _, pendingNat := range pendingNatPorts { 73 | existingNatPorts = append(existingNatPorts, pendingNat.HostSide) 74 | } 75 | 76 | assignCounter := 0 77 | 78 | for i := config.MGMTNATHI; i >= config.MGMTNATLOW; i-- { 79 | if !util.IntSliceContains(existingNatPorts, i) { 80 | natPortPairs = append(natPortPairs, &config.NatPortPair{ 81 | InstanceSide: natPorts[assignCounter], 82 | HostSide: i, 83 | }) 84 | 85 | assignCounter++ 86 | } 87 | 88 | if len(natPortPairs) == len(natPorts) { 89 | return natPortPairs, nil 90 | } 91 | } 92 | 93 | return nil, fmt.Errorf("%w: unable to allocate management nat port(s)", util.ErrAllocationError) 94 | } 95 | 96 | func (b *Boxen) allocateSocketListenPorts(numRequired int) ([]int, error) { 97 | if numRequired <= 0 { 98 | return nil, nil 99 | } 100 | 101 | existingSocketListenPorts := b.Config.AllocatedDataPlaneListenPorts() 102 | 103 | var allocatedSocketListenPorts []int 104 | 105 | for i := config.SOCKETLISTENPORTHI; i >= config.SOCKETLISTENPORTLOW; i-- { 106 | if !util.IntSliceContains(existingSocketListenPorts, i) { 107 | allocatedSocketListenPorts = append(allocatedSocketListenPorts, i) 108 | } 109 | 110 | if len(allocatedSocketListenPorts) == numRequired { 111 | return allocatedSocketListenPorts, nil 112 | } 113 | } 114 | 115 | return nil, fmt.Errorf("%w: unable to allocate socket listen port(s)", util.ErrAllocationError) 116 | } 117 | -------------------------------------------------------------------------------- /boxen/util/md5crypt.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | /* 4 | This was file written by Damian Gryski 5 | 6 | Based on the implementation at: 7 | http://code.activestate.com/recipes/325204-passwd-file-compatible-1-md5-crypt/ 8 | 9 | Licensed same as the original: 10 | 11 | Original license: 12 | * "THE BEER-WARE LICENSE" (Revision 42): 13 | * wrote this file. As long as you retain this notice you 14 | * can do whatever you want with this stuff. If we meet some day, and you think 15 | * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp 16 | 17 | This port adds no further stipulations. I forfeit any copyright interest. 18 | 19 | Damian's source -> https://github.com/dgryski/go-md5crypt/blob/master/md5crypt.go 20 | This has been modified just to appease linters and do a bit of house-keeping! 21 | */ 22 | 23 | import ( 24 | "crypto/md5" 25 | "regexp" 26 | "strings" 27 | ) 28 | 29 | const ( 30 | itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 31 | resultSize = 22 32 | ) 33 | 34 | // Md5Crypt "encrypts" a provided password byte slice. This is *not* good encryption by any stretch! 35 | // This exists in this library to encrypt plain text passwords for JunOS such that they can be sent 36 | // to the JunOS device(s) over telnet without requiring any prompting. 37 | func Md5Crypt(password []byte) []byte { 38 | md5CryptSwaps := [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} 39 | 40 | // boxen does not care about your password, sorry not sorry :) 41 | magic := []byte("$1$") 42 | salt := []byte("a") 43 | 44 | d := md5.New() 45 | 46 | d.Write(password) 47 | d.Write(magic) 48 | d.Write(salt) 49 | 50 | d2 := md5.New() 51 | d2.Write(password) 52 | d2.Write(salt) 53 | d2.Write(password) 54 | 55 | for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { 56 | d.Write([]byte{mixin[i%16]}) 57 | } 58 | 59 | for i := len(password); i != 0; i >>= 1 { 60 | if i&1 == 0 { 61 | d.Write([]byte{password[0]}) 62 | } else { 63 | d.Write([]byte{0}) 64 | } 65 | } 66 | 67 | final := d.Sum(nil) 68 | 69 | for i := 0; i < 1000; i++ { 70 | d2 := md5.New() 71 | if i&1 == 0 { 72 | d2.Write(final) 73 | } else { 74 | d2.Write(password) 75 | } 76 | 77 | if i%3 != 0 { 78 | d2.Write(salt) 79 | } 80 | 81 | if i%7 != 0 { 82 | d2.Write(password) 83 | } 84 | 85 | if i&1 == 0 { 86 | d2.Write(password) 87 | } else { 88 | d2.Write(final) 89 | } 90 | 91 | final = d2.Sum(nil) 92 | } 93 | 94 | result := make([]byte, 0, resultSize) 95 | 96 | v := uint(0) 97 | bits := uint(0) 98 | 99 | for _, i := range md5CryptSwaps { 100 | v |= (uint(final[i]) << bits) 101 | for bits += 8; bits > 6; bits -= 6 { 102 | result = append(result, itoa64[v&0x3f]) 103 | v >>= 6 104 | } 105 | } 106 | 107 | result = append(result, itoa64[v&0x3f]) 108 | 109 | return append(append(append(magic, salt...), '$'), result...) 110 | } 111 | 112 | // ConfigLinesMd5Password accepts a slice of config lines and replaces the plain-text password with 113 | // a md5 encrypted password. It does this by using the provided passwordPattern to find lines that 114 | // contain passwords. 115 | func ConfigLinesMd5Password(lines []string, passwordPattern *regexp.Regexp) []string { 116 | for i, line := range lines { 117 | matches := passwordPattern.FindStringSubmatch(line) 118 | 119 | if len(matches) > 1 { 120 | newLine := strings.Replace( 121 | line, 122 | matches[1], 123 | string(Md5Crypt([]byte(matches[1]))), 124 | 1, 125 | ) 126 | 127 | lines[i] = newLine 128 | } 129 | } 130 | 131 | return lines 132 | } 133 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | depguard: 3 | list-type: blacklist 4 | dupl: 5 | threshold: 100 6 | funlen: 7 | lines: 100 8 | statements: 50 9 | gci: 10 | local-prefixes: github.com/golangci/golangci-lint 11 | goconst: 12 | min-len: 2 13 | min-occurrences: 2 14 | gocritic: 15 | enabled-tags: 16 | - diagnostic 17 | - experimental 18 | - opinionated 19 | - performance 20 | - style 21 | disabled-checks: 22 | - dupImport # https://github.com/go-critic/go-critic/issues/845 23 | - ifElseChain 24 | - octalLiteral 25 | - whyNoLint 26 | - wrapperFunc 27 | gocyclo: 28 | min-complexity: 15 29 | goimports: 30 | local-prefixes: github.com/golangci/golangci-lint 31 | gomnd: 32 | settings: 33 | mnd: 34 | # don't include the "operation" and "assign" 35 | checks: argument,case,condition,return 36 | govet: 37 | check-shadowing: true 38 | settings: 39 | printf: 40 | funcs: 41 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 42 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 43 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 44 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 45 | lll: 46 | line-length: 100 47 | misspell: 48 | locale: US 49 | nolintlint: 50 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 51 | allow-unused: false # report any unused nolint directives 52 | require-explanation: false # don't require an explanation for nolint directives 53 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 54 | linters: 55 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 56 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 57 | disable-all: true 58 | enable: 59 | - bodyclose 60 | - deadcode 61 | - depguard 62 | - dogsled 63 | - dupl 64 | - errcheck 65 | - exhaustive 66 | - funlen 67 | - gochecknoinits 68 | - goconst 69 | - gocritic 70 | - gocyclo 71 | - gofmt 72 | - goimports 73 | - gomnd 74 | - goprintffuncname 75 | - gosec 76 | - gosimple 77 | - govet 78 | - ineffassign 79 | - lll 80 | - misspell 81 | - nakedret 82 | - noctx 83 | - nolintlint 84 | - revive 85 | - rowserrcheck 86 | - exportloopref 87 | - staticcheck 88 | - structcheck 89 | - stylecheck 90 | - typecheck 91 | - unconvert 92 | - unparam 93 | - unused 94 | - varcheck 95 | - whitespace 96 | - asciicheck 97 | - gochecknoglobals 98 | - gocognit 99 | - godot 100 | - godox 101 | - goerr113 102 | - nestif 103 | - prealloc 104 | - testpackage 105 | - wsl 106 | 107 | issues: 108 | # Excluding configuration per-path, per-linter, per-text and per-source 109 | exclude-rules: 110 | - path: util/md5crypt 111 | linters: 112 | - gosec 113 | - path: cli/start 114 | linters: 115 | - dupl 116 | - path: cli/stop 117 | linters: 118 | - dupl 119 | 120 | run: 121 | # running w/ 1.17 because we dont actually need/use 1.18 things and 1.18 breaks some linters. 122 | # actions workflows are set to 1.17 but... are 1.18 anyway? for... science? 123 | go: '1.17' 124 | skip-dirs: 125 | - private 126 | - test/testdata_etc 127 | - internal/cache 128 | - internal/renameio 129 | - internal/robustio 130 | 131 | # golangci.com configuration 132 | # https://github.com/golangci/golangci/wiki/Configuration 133 | service: 134 | golangci-lint-version: 1.45.x # use the fixed version to not introduce new linters unexpectedly 135 | prepare: 136 | - echo "here I can run custom commands, but no preparation needed for this repo" 137 | -------------------------------------------------------------------------------- /boxen/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/boxen" 7 | 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func boxenGlobalFlags() *cli.StringFlag { 12 | config := &cli.StringFlag{ 13 | Name: "config", 14 | Usage: "config file to use/create", 15 | Required: false, 16 | Value: "~/boxen/boxen.yaml", 17 | } 18 | 19 | return config 20 | } 21 | 22 | func customizationFlags() (username, password, hostname *cli.StringFlag) { 23 | username = &cli.StringFlag{ 24 | Name: "username", 25 | Usage: "username to set on the instance", 26 | Required: false, 27 | } 28 | 29 | password = &cli.StringFlag{ 30 | Name: "password", 31 | Usage: "password to set on the instance", 32 | Required: false, 33 | } 34 | 35 | hostname = &cli.StringFlag{ 36 | Name: "hostname", 37 | Usage: "hostname to set on the device", 38 | Required: false, 39 | } 40 | 41 | return username, password, hostname 42 | } 43 | 44 | func platformTargetFlags() (vendor, platform, version *cli.StringFlag) { 45 | vendor = &cli.StringFlag{ 46 | Name: "vendor", 47 | Usage: "name of the disks vendor (ex: 'arista')", 48 | Required: false, 49 | } 50 | 51 | platform = &cli.StringFlag{ 52 | Name: "platform", 53 | Usage: "name of the disks platform (ex: 'veos')", 54 | Required: false, 55 | } 56 | 57 | version = &cli.StringFlag{ 58 | Name: "version", 59 | Usage: "version of the disk (ex: '4.22.1F')", 60 | Required: false, 61 | } 62 | 63 | return vendor, platform, version 64 | } 65 | 66 | func operationCommands() []*cli.Command { 67 | config := boxenGlobalFlags() 68 | 69 | instances := &cli.StringFlag{ 70 | Name: "instances", 71 | Usage: "instance or comma sep string of instances to start/stop", 72 | Required: false, 73 | } 74 | group := &cli.StringFlag{ 75 | Name: "group", 76 | Usage: "name of instance group to start/stop", 77 | Required: false, 78 | } 79 | 80 | return []*cli.Command{ 81 | { //nolint:dupl 82 | Name: "start", 83 | Usage: "start boxen instance(s)/group(s)", 84 | Subcommands: []*cli.Command{ 85 | { 86 | Name: "instance", 87 | Usage: "start boxen instance(s)", 88 | Flags: []cli.Flag{ 89 | config, 90 | instances, 91 | }, 92 | Action: func(c *cli.Context) error { 93 | return Start(c.String("config"), c.String("instances")) 94 | }, 95 | }, 96 | { 97 | Name: "group", 98 | Usage: "start boxen group", 99 | Flags: []cli.Flag{ 100 | config, 101 | group, 102 | }, 103 | Action: func(c *cli.Context) error { 104 | return StartGroup(c.String("config"), c.String("group")) 105 | }, 106 | }, 107 | }, 108 | }, 109 | { //nolint:dupl 110 | Name: "stop", 111 | Usage: "stop boxen instance(s)/group(s)", 112 | Subcommands: []*cli.Command{ 113 | { 114 | Name: "instance", 115 | Usage: "stop boxen instance(s)", 116 | Flags: []cli.Flag{ 117 | config, 118 | instances, 119 | }, 120 | Action: func(c *cli.Context) error { 121 | return Stop(c.String("config"), c.String("instances")) 122 | }, 123 | }, 124 | { 125 | Name: "group", 126 | Usage: "stop boxen group", 127 | Flags: []cli.Flag{ 128 | config, 129 | group, 130 | }, 131 | Action: func(c *cli.Context) error { 132 | return StopGroup(c.String("config"), c.String("group")) 133 | }, 134 | }, 135 | }, 136 | }, 137 | } 138 | } 139 | 140 | func NewCLI() *cli.App { 141 | cli.VersionPrinter = showVersion 142 | 143 | var commands []*cli.Command 144 | 145 | commands = append(commands, initCommands()...) 146 | commands = append(commands, packageBuildCommands()...) 147 | commands = append(commands, packageInstallCommands()...) 148 | commands = append(commands, packageStartCommands()...) 149 | commands = append(commands, installCommands()...) 150 | commands = append(commands, unInstallCommands()...) 151 | commands = append(commands, provisionCommands()...) 152 | commands = append(commands, deProvisionCommands()...) 153 | commands = append(commands, operationCommands()...) 154 | 155 | app := &cli.App{ 156 | Name: "boxen", 157 | Version: "dev", 158 | Usage: "package or run network operating system vm instances", 159 | Commands: commands, 160 | } 161 | 162 | return app 163 | } 164 | 165 | func showVersion(c *cli.Context) { 166 | fmt.Printf("\tversion: %s\n", boxen.Version) 167 | fmt.Printf("\tsource: %s\n", "https://github.com/carlmontanari/boxen") 168 | } 169 | -------------------------------------------------------------------------------- /boxen/boxen/common.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/carlmontanari/boxen/boxen/platforms" 10 | 11 | "github.com/carlmontanari/boxen/boxen" 12 | "github.com/carlmontanari/boxen/boxen/command" 13 | "github.com/carlmontanari/boxen/boxen/config" 14 | "github.com/carlmontanari/boxen/boxen/util" 15 | 16 | "gopkg.in/yaml.v2" 17 | ) 18 | 19 | var Version = "0.0.0" //nolint: gochecknoglobals 20 | 21 | type Disk struct { 22 | Disk string 23 | Vendor string 24 | Platform string 25 | PlatformType string 26 | Version string 27 | } 28 | 29 | func (b *Boxen) sourceDiskExists(pT, diskVersion string) bool { 30 | v, ok := b.Config.Platforms[pT] 31 | if !ok { 32 | return false 33 | } 34 | 35 | if util.StringSliceContains( 36 | diskVersion, 37 | v.SourceDisks, 38 | ) { 39 | return true 40 | } 41 | 42 | return false 43 | } 44 | 45 | func getDiskData(i *installInfo) error { 46 | f, err := util.ResolveFile(i.inDisk) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // if the disk object has already been set (because user provided the values) we can just 52 | // set the resolved disk path then bail out of here. 53 | if i.srcDisk != nil { 54 | i.srcDisk.Disk = f 55 | 56 | return nil 57 | } 58 | 59 | i.srcDisk = &Disk{} 60 | i.srcDisk.Disk = f 61 | 62 | v, p, err := platforms.GetPlatformTypeFromDisk(filepath.Base(f)) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | i.srcDisk.Vendor = v 68 | i.srcDisk.Platform = p 69 | i.srcDisk.PlatformType = platforms.GetPlatformType(i.srcDisk.Vendor, i.srcDisk.Platform) 70 | 71 | dV, err := platforms.GetDiskVersion(filepath.Base(f), i.srcDisk.PlatformType) 72 | i.srcDisk.Version = dV 73 | 74 | return err 75 | } 76 | 77 | // GetDefaultProfile fetches the default instance profile for a given platform type 'pt'. 78 | func GetDefaultProfile(pt string) (*config.Profile, error) { 79 | var f []byte 80 | 81 | var err error 82 | 83 | envProfilePath := os.Getenv(fmt.Sprintf("BOXEN_%s_PROFILE", strings.ToUpper(pt))) 84 | if envProfilePath != "" { 85 | f, err = os.ReadFile(envProfilePath) 86 | } else { 87 | f, err = boxen.Assets.ReadFile(fmt.Sprintf("assets/profiles/%s.yaml", pt)) 88 | } 89 | 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | profile := &config.Profile{} 95 | 96 | err = yaml.UnmarshalStrict(f, profile) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return profile, nil 102 | } 103 | 104 | func (b *Boxen) installAllocateDisks(i *installInfo) error { 105 | err := getDiskData(i) 106 | if err != nil { 107 | msg := fmt.Sprintf("failed gleaning source data from disk '%s'", i.inDisk) 108 | 109 | b.Logger.Critical(msg) 110 | 111 | return fmt.Errorf( 112 | "%w: %s", 113 | util.ErrInspectionError, 114 | msg, 115 | ) 116 | } 117 | 118 | i.name = fmt.Sprintf("%s_%s", i.srcDisk.PlatformType, i.srcDisk.Version) 119 | i.newDisk = fmt.Sprintf("%s/%s.qcow2", i.tmpDir, i.name) 120 | 121 | switch { 122 | // if the qemu-img command is not present, use docker 123 | case !util.CommandExists(util.QemuImgCmd): 124 | _, err = command.Execute( 125 | util.DockerCmd, 126 | command.WithArgs( 127 | []string{ 128 | "run", 129 | "-v", 130 | i.srcDisk.Disk + ":/src", 131 | "-v", 132 | i.tmpDir + ":/dst", 133 | util.QemuImgContainer, 134 | "convert", 135 | "-O", 136 | "qcow2", 137 | "/src", 138 | "/dst/" + i.name + ".qcow2", 139 | }, 140 | ), 141 | command.WithWait(true), 142 | ) 143 | default: 144 | _, err = command.Execute( 145 | util.QemuImgCmd, 146 | command.WithArgs( 147 | []string{"convert", "-O", "qcow2", i.srcDisk.Disk, i.newDisk}, 148 | ), 149 | command.WithWait(true), 150 | ) 151 | } 152 | 153 | if err != nil { 154 | b.Logger.Criticalf("error copying source disk image: %s\n", err) 155 | 156 | return err 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (b *Boxen) handleProvidedPlatformInfo(i *installInfo, vendor, platform, version string) error { 163 | if !util.AllStringVal("", vendor, platform, version) { 164 | if util.AnyStringVal("", vendor, platform, version) { 165 | return fmt.Errorf("%w: one or more of vendor, platform, version set, "+ 166 | "but not all values provided, if explicitly targeting a specific "+ 167 | " vendor/platform/version you must provide all values", 168 | util.ErrValidationError) 169 | } 170 | 171 | pT := platforms.GetPlatformType(vendor, platform) 172 | 173 | if pT == "" { 174 | return fmt.Errorf("%w: provided vendor/platform '%s'/'%s' not supported", 175 | util.ErrValidationError, vendor, platform) 176 | } 177 | 178 | i.srcDisk = &Disk{ 179 | Vendor: vendor, 180 | Platform: platform, 181 | PlatformType: pT, 182 | Version: version, 183 | } 184 | } 185 | 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /boxen/boxen/provision.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/carlmontanari/boxen/boxen/config" 7 | "github.com/carlmontanari/boxen/boxen/util" 8 | ) 9 | 10 | func (b *Boxen) provision( 11 | name, platformType, sourceDisk, profileName string, 12 | profileObj *config.Profile, 13 | ) error { 14 | instanceID, err := b.allocateInstanceID() 15 | if err != nil { 16 | b.Logger.Critical("failed allocating instance ID") 17 | 18 | return err 19 | } 20 | 21 | serialPorts, err := b.allocateSerialPorts( 22 | profileObj.Hardware.SerialPortCount, 23 | instanceID, 24 | ) 25 | if err != nil { 26 | b.Logger.Critical("failed to allocate serial port ID(s)") 27 | 28 | return err 29 | } 30 | 31 | monitorPort := b.allocateMonitorPort(instanceID) 32 | 33 | socketListenPorts, err := b.allocateSocketListenPorts(profileObj.Hardware.NicCount) 34 | if err != nil { 35 | b.Logger.Critical("failed to allocate socket listen ports") 36 | 37 | return err 38 | } 39 | 40 | dataPlaneIntfMap := make(map[int]*config.SocketConnectPair) 41 | for i, val := range socketListenPorts { 42 | dataPlaneIntfMap[i+1] = &config.SocketConnectPair{ 43 | Connect: -1, 44 | Listen: val, 45 | } 46 | } 47 | 48 | tcpNats, err := b.allocateMgmtNatPorts(profileObj.TPCNatPorts, nil) 49 | if err != nil { 50 | b.Logger.Critical("failed to allocate management tcp nat ports") 51 | 52 | return err 53 | } 54 | 55 | updNats, err := b.allocateMgmtNatPorts(profileObj.UDPNatPorts, tcpNats) 56 | if err != nil { 57 | b.Logger.Critical("failed to allocate management udp nat ports") 58 | 59 | return err 60 | } 61 | 62 | hw := profileObj.Hardware.ToHardware() 63 | hw.SerialPorts = serialPorts 64 | hw.MonitorPort = monitorPort 65 | 66 | b.Config.AddInstance(name, &config.Instance{ 67 | Name: name, 68 | PlatformType: platformType, 69 | Disk: sourceDisk, 70 | ID: instanceID, 71 | PID: 0, 72 | Profile: profileName, 73 | Credentials: config.NewDefaultCredentials(), 74 | Hardware: hw, 75 | MgmtIntf: &config.MgmtIntf{ 76 | Nat: &config.Nat{ 77 | TCP: tcpNats, 78 | UDP: updNats, 79 | }, 80 | Bridge: nil, 81 | }, 82 | DataPlaneIntf: &config.DataPlaneIntf{SocketConnectMap: dataPlaneIntfMap}, 83 | Advanced: profileObj.Advanced, 84 | BootDelay: 0, 85 | }) 86 | 87 | return nil 88 | } 89 | 90 | // Provision creates all the required config objects/port allocation/etc. for a local boxen 91 | // instance. 92 | func (b *Boxen) Provision(instance, vendor, platform, sourceDisk, profile string) error { 93 | b.Logger.Infof( 94 | "provision instance '%s' of vendor '%s' platform '%s' requested", 95 | instance, 96 | vendor, 97 | platform, 98 | ) 99 | 100 | _, ok := b.Config.Instances[instance] 101 | if ok { 102 | return fmt.Errorf( 103 | "%w: instance named '%s' already exists, cannot provision", 104 | util.ErrAllocationError, 105 | instance, 106 | ) 107 | } 108 | 109 | platformType := fmt.Sprintf("%s_%s", vendor, platform) 110 | 111 | _, ok = b.Config.Platforms[platformType] 112 | if !ok { 113 | return fmt.Errorf( 114 | "%w: no platform type registered for type '%s', cannot proceed", 115 | util.ErrAllocationError, 116 | platformType, 117 | ) 118 | } 119 | 120 | sourceDisks := b.Config.Platforms[platformType].SourceDisks 121 | if sourceDisk == "" { 122 | if len(sourceDisks) == 0 { 123 | sourceDisk = "" 124 | 125 | b.Logger.Debug( 126 | "no source disk provided, and no source disks available, this is gunna be a bad time", 127 | ) 128 | } else { 129 | sourceDisk = sourceDisks[0] 130 | 131 | b.Logger.Debugf("no source disk provided, using '%s'", sourceDisk) 132 | } 133 | } 134 | 135 | if !util.StringSliceContains(sourceDisk, sourceDisks) { 136 | msg := fmt.Sprintf( 137 | "source disk '%s' does not exist for platform, cannot proceed", 138 | sourceDisk, 139 | ) 140 | 141 | b.Logger.Criticalf(msg) 142 | 143 | return fmt.Errorf("%w: %s", util.ErrProvisionError, msg) 144 | } 145 | 146 | if profile == "" { 147 | profile = "default" 148 | 149 | b.Logger.Debugf("no profile name provided, using '%s'", profile) 150 | } 151 | 152 | profileObj, ok := b.Config.Platforms[platformType].Profiles[profile] 153 | if !ok { 154 | msg := fmt.Sprintf("profile '%s' does not exist for platform cannot proceed", profile) 155 | 156 | b.Logger.Criticalf(msg) 157 | 158 | return fmt.Errorf("%w: %s", util.ErrProvisionError, msg) 159 | } 160 | 161 | err := b.provision(instance, platformType, sourceDisk, profile, profileObj) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | b.Logger.Info("provisioning instance(s) completed successfully") 167 | 168 | err = b.Config.Dump(b.ConfigPath) 169 | if err != nil { 170 | b.Logger.Criticalf("error dumping boxen initial config to disk: %s", err) 171 | return err 172 | } 173 | 174 | b.Logger.Info("provision instance(s) completed successfully") 175 | 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /boxen/boxen/packagestart.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/carlmontanari/boxen/boxen/command" 9 | "github.com/carlmontanari/boxen/boxen/instance" 10 | "github.com/carlmontanari/boxen/boxen/platforms" 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | ) 13 | 14 | func (b *Boxen) clabNicProvisionDelay() error { 15 | clabIntfs := util.GetEnvIntOrDefault("CLAB_INTFS", 0) 16 | 17 | if clabIntfs == 0 { 18 | return nil 19 | } 20 | 21 | b.Logger.Info("waiting until clab nics show up") 22 | 23 | intfGlob := "/sys/class/net/eth*" 24 | 25 | for { 26 | provisionedNics, err := filepath.Glob(intfGlob) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // clab intfs + 1 for mgmt 32 | if len(provisionedNics) >= clabIntfs+1 { 33 | b.Logger.Debug("clab nics available, moving on") 34 | 35 | return nil 36 | } 37 | 38 | time.Sleep(5 * time.Second) //nolint:gomnd 39 | } 40 | } 41 | 42 | // packageSocat launches the background socat tasks for the packaged instance. 43 | func (b *Boxen) packageSocat(name string) error { 44 | b.Logger.Debug("launching socat processes") 45 | 46 | tcpPortPairs := b.Config.Instances[name].MgmtIntf.Nat.TCP 47 | udpPortPairs := b.Config.Instances[name].MgmtIntf.Nat.UDP 48 | 49 | for _, tcpPortPair := range tcpPortPairs { 50 | _, err := command.Execute("socat", command.WithArgs([]string{ 51 | fmt.Sprintf("TCP-LISTEN:%d,fork", tcpPortPair.InstanceSide), 52 | fmt.Sprintf("TCP:127.0.0.1:%d", tcpPortPair.HostSide), 53 | })) 54 | if err != nil { 55 | b.Logger.Criticalf("error launching management tcp socat command: %s\n", err) 56 | 57 | return err 58 | } 59 | } 60 | 61 | for _, udpPortPair := range udpPortPairs { 62 | _, err := command.Execute("socat", command.WithArgs([]string{ 63 | fmt.Sprintf("UDP-LISTEN:%d,fork", udpPortPair.InstanceSide), 64 | fmt.Sprintf("UDP:127.0.0.1:%d", udpPortPair.HostSide), 65 | })) 66 | if err != nil { 67 | b.Logger.Criticalf("error launching management udp socat command: %s\n", err) 68 | 69 | return err 70 | } 71 | } 72 | 73 | b.Logger.Debug("socat processes launched...") 74 | 75 | return nil 76 | } 77 | 78 | func (b *Boxen) clabStartDelay() { 79 | startDelay := util.GetEnvIntOrDefault( 80 | "BOOT_DELAY", 81 | 0, 82 | ) 83 | 84 | if startDelay != 0 { 85 | b.Logger.Infof("start delay set, sleeping for %d seconds...", startDelay) 86 | time.Sleep(time.Duration(startDelay) * time.Second) 87 | 88 | b.Logger.Debug("start delay complete, continuing") 89 | } 90 | } 91 | 92 | func (b *Boxen) packageStartConfig(name, username, password, hostname, config string) error { 93 | startupConfig := util.GetEnvStrOrDefault( 94 | "STARTUP_CONFIG", 95 | "", 96 | ) 97 | 98 | saveRequired := false 99 | 100 | f := startupConfig 101 | if config != "" { 102 | f = config 103 | } 104 | 105 | if f != "" { //nolint:nestif 106 | err := b.Instances[name].InstallConfig(f, true) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | saveRequired = true 112 | } else { 113 | if username != "" && password != "" { 114 | err := b.Instances[name].SetUserPass(username, password) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | saveRequired = true 120 | } 121 | 122 | if hostname != "" { 123 | err := b.Instances[name].SetHostname(hostname) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | saveRequired = true 129 | } 130 | } 131 | 132 | if saveRequired { 133 | err := b.Instances[name].SaveConfig() 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // PackageStart starts the qemu instance vm inside a packaged boxen container. 143 | func (b *Boxen) PackageStart(username, password, hostname, config string) error { 144 | b.Logger.Info("package start requested") 145 | 146 | name := b.getPackagedInstanceName() 147 | 148 | instanceLoggers, err := instance.NewInstanceLoggersFOut(b.Logger, "/") 149 | if err != nil { 150 | return err 151 | } 152 | 153 | q, err := platforms.NewPlatformFromConfig( 154 | name, 155 | b.Config, 156 | instanceLoggers, 157 | ) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | err = b.clabNicProvisionDelay() 163 | if err != nil { 164 | return err 165 | } 166 | 167 | err = b.packageSocat(name) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | b.clabStartDelay() 173 | 174 | b.Instances[name] = q 175 | 176 | err = b.Instances[name].Start( 177 | // with prepare console so that the console has paging disabled and all that good stuff 178 | // so, we can set the hostname/username/password if desired. 179 | platforms.WithPrepareConsole(true), 180 | ) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | err = b.packageStartConfig(name, username, password, hostname, config) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | err = b.Instances[name].Detach() 191 | if err != nil { 192 | return err 193 | } 194 | 195 | b.Logger.Info("package start completed successfully, running until signal interrupt") 196 | 197 | b.Instances[name].RunUntilSigInt() 198 | 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/carlmontanari/difflibgo v0.0.0-20210718194309-31b9e131c298 h1:Y8rTum6LZ8oP/2aC+OaaP76OCjHbunKMkim81mzNCH0= 3 | github.com/carlmontanari/difflibgo v0.0.0-20210718194309-31b9e131c298/go.mod h1:+3MuSIeC3qmdSesR12cTLeb47R/Vvo+bHdB6hC5HShk= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 8 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 9 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 10 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 12 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 15 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 16 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 19 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 23 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 24 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 25 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 26 | github.com/scrapli/scrapligo v1.0.0/go.mod h1:jvRMdb90MNnswMiku8UNXj8JZaOIPhwhcqqFwr9qeoY= 27 | github.com/scrapli/scrapligo v1.1.0 h1:KjCam57kIV2rlxAQg/J1G7v/xgRHvpJF+Gjz+LXhQaI= 28 | github.com/scrapli/scrapligo v1.1.0/go.mod h1:jvRMdb90MNnswMiku8UNXj8JZaOIPhwhcqqFwr9qeoY= 29 | github.com/scrapli/scrapligocfg v1.0.0 h1:540SuGqqM6rKN87SLCfR54IageQ6s3a/ZOycGRgbbak= 30 | github.com/scrapli/scrapligocfg v1.0.0/go.mod h1:9+6k9dQeIqEZEg6EK5YXEjuVb7h+nvvel26CY1RGjy4= 31 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 32 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 33 | github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 h1:FHUL2HofYJuslFOQdy/JjjP36zxqIpd/dcoiwLMIs7k= 34 | github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4/go.mod h1:CJYqpTg9u5VPCoD0VEl9E68prCIiWQD8m457k098DdQ= 35 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 36 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 37 | golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= 38 | golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 39 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 40 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= 44 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 46 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 47 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 48 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 49 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 52 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 54 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 56 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 57 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 58 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | -------------------------------------------------------------------------------- /boxen/platforms/checkpoint_cloudguard.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | sopoptions "github.com/scrapli/scrapligo/driver/opoptions" 8 | 9 | "github.com/carlmontanari/boxen/boxen/instance" 10 | ) 11 | 12 | const ( 13 | CheckpointCloudguardDefaultUser = "admin" 14 | CheckpointCloudguardDefaultPass = "admin" 15 | 16 | CheckpointCloudguardScrapliPlatform = "https://gist.githubusercontent.com/hellt/1eee1024bc1cb3121aaeac199d48663a/raw/07caf0b024802da2dbb6fe17dbabcb26231b8cb6/checkpoint_cloudguard.yaml" // nolint:lll 17 | 18 | checkpointCloudGuardDefaultBootTime = 720 19 | ) 20 | 21 | type CheckpointCloudguard struct { 22 | *instance.Qemu 23 | *ScrapliConsole 24 | } 25 | 26 | func (p *CheckpointCloudguard) Package( 27 | _, _ string, 28 | ) (packageFiles, runFiles []string, err error) { 29 | return nil, nil, err 30 | } 31 | 32 | func (p *CheckpointCloudguard) Install(opts ...instance.Option) error { 33 | p.Loggers.Base.Info("install requested") 34 | 35 | a, opts, err := setInstallArgs(opts...) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | c := make(chan error, 1) 41 | stop := make(chan bool, 1) 42 | 43 | go func() { //nolint:dupl 44 | err = p.Qemu.Start(opts...) 45 | if err != nil { 46 | c <- err 47 | } 48 | 49 | p.Loggers.Base.Debug("instance started, waiting for start ready state") 50 | 51 | err = p.startReady() 52 | if err != nil { 53 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 54 | 55 | c <- err 56 | } 57 | 58 | p.Loggers.Base.Debug("start ready state acquired, logging in") 59 | 60 | err = p.login( 61 | &loginArgs{ 62 | username: CheckpointCloudguardDefaultUser, 63 | password: CheckpointCloudguardDefaultPass, 64 | }, 65 | ) 66 | if err != nil { 67 | c <- err 68 | } 69 | 70 | p.Loggers.Base.Debug("log in complete") 71 | 72 | if a.configLines != nil { 73 | p.Loggers.Base.Debug("install config lines provided, executing scrapligo on open") 74 | 75 | err = p.defOnOpen(p.c) 76 | if err != nil { 77 | p.Loggers.Base.Criticalf("error running scrapligo on open: %s\n", err) 78 | 79 | c <- err 80 | } 81 | 82 | err = p.Config(a.configLines) 83 | if err != nil { 84 | p.Loggers.Base.Criticalf("error sending install config lines: %s\n", err) 85 | 86 | c <- err 87 | } 88 | } 89 | 90 | p.Loggers.Base.Debug("initial installation complete") 91 | 92 | err = p.SaveConfig() 93 | if err != nil { 94 | p.Loggers.Base.Criticalf("error saving config: %s\n", err) 95 | 96 | c <- err 97 | } 98 | 99 | // small delay ensuring config is saved nicely, without this extra sleep things just seem to 100 | // not actually "save" despite the "save complete" or whatever output. 101 | time.Sleep(5 * time.Second) // nolint:gomnd 102 | 103 | c <- nil 104 | stop <- true 105 | }() 106 | 107 | go p.WatchMainProc(c, stop) 108 | 109 | err = <-c 110 | if err != nil { 111 | return err 112 | } 113 | 114 | p.Loggers.Base.Info("install complete, stopping instance") 115 | 116 | return p.Stop(opts...) 117 | } 118 | 119 | func (p *CheckpointCloudguard) Start(opts ...instance.Option) error { // nolint:dupl 120 | p.Loggers.Base.Info("start platform instance requested") 121 | 122 | a, opts, err := setStartArgs(opts...) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | err = p.Qemu.Start(opts...) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | err = p.startReady() 133 | if err != nil { 134 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 135 | 136 | return err 137 | } 138 | 139 | if !a.prepareConsole { 140 | p.Loggers.Base.Info("prepare console not requested, starting instance complete") 141 | 142 | return nil 143 | } 144 | 145 | err = p.login( 146 | &loginArgs{ 147 | username: CheckpointCloudguardDefaultUser, 148 | password: CheckpointCloudguardDefaultPass, 149 | }, 150 | ) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | err = p.defOnOpen(p.c) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | p.Loggers.Base.Info("starting platform instance complete") 161 | 162 | return nil 163 | } 164 | 165 | func (p *CheckpointCloudguard) startReady() error { 166 | // openRetry doesn't do auth and doesn't call onOpen as it is set to nil somewhere before this 167 | err := p.openRetry() 168 | if err != nil { 169 | return err 170 | } 171 | 172 | err = p.readUntil( 173 | []byte("This system is for authorized use only"), 174 | getPlatformBootTimeout(PlatformTypeCheckpointCloudguard), 175 | ) 176 | 177 | return err 178 | } 179 | 180 | func (p *CheckpointCloudguard) SaveConfig() error { 181 | p.Loggers.Base.Info("save config requested") 182 | 183 | _, err := p.c.SendCommand( 184 | "save config", 185 | sopoptions.WithTimeoutOps( 186 | time.Duration(getPlatformSaveTimeout(PlatformTypeCheckpointCloudguard))*time.Second, 187 | ), 188 | ) 189 | 190 | return err 191 | } 192 | 193 | func (p *CheckpointCloudguard) SetUserPass(usr, pwd string) error { 194 | if usr == CheckpointCloudguardDefaultPass && pwd == CheckpointCloudguardDefaultPass { 195 | p.Loggers.Base.Info("skipping user creation, since credentials match defaults for platform") 196 | return nil 197 | } 198 | 199 | p.Loggers.Base.Infof("set user/password for user '%s' requested", usr) 200 | 201 | return p.Config([]string{ 202 | fmt.Sprintf( 203 | "add user %s uid 0 homedir /home/%s", 204 | usr, 205 | usr), 206 | fmt.Sprintf( 207 | "add rba user %s roles adminRole", 208 | usr), 209 | fmt.Sprintf( 210 | "set user %s newpass %s", 211 | usr, 212 | pwd), 213 | }) 214 | } 215 | 216 | func (p *CheckpointCloudguard) SetHostname(h string) error { 217 | p.Loggers.Base.Infof("set hostname '%s' requested", h) 218 | 219 | return p.Config([]string{fmt.Sprintf("set hostname %s", h)}) 220 | } 221 | -------------------------------------------------------------------------------- /boxen/platforms/ipinfusion_ocnos.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | sopoptions "github.com/scrapli/scrapligo/driver/opoptions" 8 | 9 | "github.com/carlmontanari/boxen/boxen/instance" 10 | ) 11 | 12 | const ( 13 | IPInfusionOcNOSScrapliPlatform = "ipinfusion_ocnos" 14 | IPInfusionOcNOSDefaultUser = "ocnos" 15 | IPInfusionOcNOSDefaultPass = "ocnos" 16 | ) 17 | 18 | type IPInfusionOcNOS struct { 19 | *instance.Qemu 20 | *ScrapliConsole 21 | } 22 | 23 | func (p *IPInfusionOcNOS) Package( 24 | _, _ string, 25 | ) (packageFiles, runFiles []string, err error) { 26 | return nil, nil, err 27 | } 28 | 29 | func (p *IPInfusionOcNOS) Install(opts ...instance.Option) error { 30 | p.Loggers.Base.Info("install requested") 31 | 32 | a, opts, err := setInstallArgs(opts...) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | c := make(chan error, 1) 38 | stop := make(chan bool, 1) 39 | 40 | go func() { 41 | err = p.Qemu.Start(opts...) 42 | if err != nil { 43 | c <- err 44 | } 45 | 46 | p.Loggers.Base.Debug("instance started, waiting for start ready state") 47 | 48 | err = p.startReady() 49 | if err != nil { 50 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 51 | 52 | c <- err 53 | } 54 | 55 | p.Loggers.Base.Debug("start ready state acquired, logging in") 56 | 57 | err = p.login( 58 | &loginArgs{ 59 | username: IPInfusionOcNOSDefaultUser, 60 | password: IPInfusionOcNOSDefaultPass, 61 | }, 62 | ) 63 | if err != nil { 64 | c <- err 65 | } 66 | 67 | p.Loggers.Base.Debug("log in complete") 68 | 69 | if a.configLines != nil { 70 | p.Loggers.Base.Debug("install config lines provided, executing scrapligo on open") 71 | 72 | err = p.defOnOpen(p.c) 73 | if err != nil { 74 | p.Loggers.Base.Criticalf("error running scrapligo on open: %s\n", err) 75 | 76 | c <- err 77 | } 78 | 79 | err = p.Config(a.configLines) 80 | if err != nil { 81 | p.Loggers.Base.Criticalf("error sending install config lines: %s\n", err) 82 | 83 | c <- err 84 | } 85 | 86 | // issue a commit which is required by ocnos v5+, but is not needed in earlier version 87 | // thus the error is not checked 88 | p.Config([]string{"commit"}) // nolint:errcheck 89 | } 90 | 91 | p.Loggers.Base.Debug("initial installation complete") 92 | 93 | err = p.SaveConfig() 94 | if err != nil { 95 | p.Loggers.Base.Criticalf("error saving config: %s\n", err) 96 | 97 | c <- err 98 | } 99 | 100 | // small delay ensuring config is saved nicely, without this extra sleep things just seem to 101 | // not actually "save" despite the "save complete" or whatever output. 102 | time.Sleep(5 * time.Second) // nolint:gomnd 103 | 104 | c <- nil 105 | stop <- true 106 | }() 107 | 108 | go p.WatchMainProc(c, stop) 109 | 110 | err = <-c 111 | if err != nil { 112 | return err 113 | } 114 | 115 | p.Loggers.Base.Info("install complete, stopping instance") 116 | 117 | return p.Stop(opts...) 118 | } 119 | 120 | func (p *IPInfusionOcNOS) Start(opts ...instance.Option) error { // nolint:dupl 121 | p.Loggers.Base.Info("start platform instance requested") 122 | 123 | a, opts, err := setStartArgs(opts...) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | err = p.Qemu.Start(opts...) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | err = p.startReady() 134 | if err != nil { 135 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 136 | 137 | return err 138 | } 139 | 140 | if !a.prepareConsole { 141 | p.Loggers.Base.Info("prepare console not requested, starting instance complete") 142 | 143 | return nil 144 | } 145 | 146 | err = p.login( 147 | &loginArgs{ 148 | username: IPInfusionOcNOSDefaultUser, 149 | password: IPInfusionOcNOSDefaultPass, 150 | }, 151 | ) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | err = p.defOnOpen(p.c) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | p.Loggers.Base.Info("starting platform instance complete") 162 | 163 | return nil 164 | } 165 | 166 | func (p *IPInfusionOcNOS) startReady() error { 167 | // openRetry doesn't do auth and doesn't call onOpen as it is set to nil somewhere before this 168 | err := p.openRetry() 169 | if err != nil { 170 | return err 171 | } 172 | 173 | err = p.readUntil( 174 | []byte("Welcome to OcNOS"), 175 | getPlatformBootTimeout(PlatformTypeIPInfusionOcNOS), 176 | ) 177 | 178 | return err 179 | } 180 | 181 | func (p *IPInfusionOcNOS) SaveConfig() error { 182 | p.Loggers.Base.Info("save config requested") 183 | 184 | _, err := p.c.SendCommand( 185 | "copy running-config startup-config", 186 | sopoptions.WithTimeoutOps( 187 | time.Duration(getPlatformSaveTimeout(PlatformTypeIPInfusionOcNOS))*time.Second, 188 | ), 189 | ) 190 | 191 | return err 192 | } 193 | 194 | func (p *IPInfusionOcNOS) SetUserPass(usr, pwd string) error { 195 | p.Loggers.Base.Infof("set user/password for user '%s' requested", usr) 196 | 197 | err := p.Config([]string{fmt.Sprintf( 198 | "username %s role network-admin password %s", 199 | usr, 200 | pwd)}) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // issue a commit which is required by ocnos v5+, but is not needed in earlier version 206 | // thus the error is not checked 207 | p.Config([]string{"commit"}) // nolint:errcheck 208 | 209 | return err 210 | } 211 | 212 | func (p *IPInfusionOcNOS) SetHostname(h string) error { 213 | p.Loggers.Base.Infof("set hostname '%s' requested", h) 214 | 215 | err := p.Config([]string{fmt.Sprintf( 216 | "hostname %s", 217 | h)}) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | // issue a commit which is required by ocnos v5+, but is not needed in earlier version 223 | // thus the error is not checked 224 | p.Config([]string{"commit"}) // nolint:errcheck 225 | 226 | return err 227 | } 228 | -------------------------------------------------------------------------------- /boxen/boxen/start.go: -------------------------------------------------------------------------------- 1 | package boxen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/carlmontanari/boxen/boxen/command" 10 | "github.com/carlmontanari/boxen/boxen/instance" 11 | "github.com/carlmontanari/boxen/boxen/platforms" 12 | "github.com/carlmontanari/boxen/boxen/util" 13 | ) 14 | 15 | func (b *Boxen) copySourceDiskToInstanceDir(name, disk string) error { 16 | var err error 17 | 18 | if b.Config.Options.Qemu.UseThickDisks { 19 | err = util.CopyFile( 20 | fmt.Sprintf( 21 | "%s/%s/%s/disk.qcow2", 22 | b.Config.Options.Build.SourcePath, 23 | b.Config.Instances[name].PlatformType, 24 | b.Config.Instances[name].Disk, 25 | ), 26 | disk, 27 | ) 28 | } else { 29 | _, err = command.Execute( 30 | util.QemuImgCmd, 31 | command.WithArgs( 32 | []string{"create", "-f", "qcow2", "-F", "qcow2", "-b", fmt.Sprintf( 33 | "%s/%s/%s/disk.qcow2", 34 | b.Config.Options.Build.SourcePath, 35 | b.Config.Instances[name].PlatformType, 36 | b.Config.Instances[name].Disk, 37 | ), disk}, 38 | ), 39 | command.WithWait(true), 40 | ) 41 | } 42 | 43 | if err != nil { 44 | b.Logger.Criticalf("error creating instance disk: %s\n", err) 45 | 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (b *Boxen) copySourceRunFilesToInstanceDir(name, disk string) error { 53 | runFiles, err := filepath.Glob( 54 | fmt.Sprintf("%s/%s/%s/[^disk.qcow2]*", b.Config.Options.Build.SourcePath, 55 | b.Config.Instances[name].PlatformType, 56 | b.Config.Instances[name].Disk), 57 | ) 58 | 59 | if err != nil { 60 | b.Logger.Criticalf("error globbing run files associated with disk: %s\n", err) 61 | 62 | return err 63 | } 64 | 65 | for _, f := range runFiles { 66 | err = util.CopyFile(f, fmt.Sprintf("%s/%s", filepath.Dir(disk), filepath.Base(f))) 67 | 68 | if err != nil { 69 | b.Logger.Criticalf( 70 | "error copying run file '%s' to instance directory: %s\n", 71 | f, 72 | err, 73 | ) 74 | 75 | return err 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (b *Boxen) startCheckDisk(name, disk string) error { 83 | var err error 84 | 85 | diskExists := util.FileExists(disk) 86 | 87 | // in the future we will panic/err out if the disk exists and the persist mode (not implemented 88 | // yet) is false. for now, we will just copy the disk over if it doesn't exist. 89 | 90 | if !diskExists { 91 | err = b.copySourceDiskToInstanceDir(name, disk) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | err = b.copySourceRunFilesToInstanceDir(name, disk) 97 | if err != nil { 98 | return err 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // Start starts a local boxen instance. 106 | func (b *Boxen) Start(name string) error { 107 | b.Logger.Infof("start for instance '%s' requested", name) 108 | 109 | _, ok := b.Config.Instances[name] 110 | if !ok { 111 | return fmt.Errorf("%w: no instance name '%s' in the config", util.ErrInstanceError, name) 112 | } 113 | 114 | instanceDir := fmt.Sprintf("%s/%s", b.Config.Options.Build.InstancePath, name) 115 | instanceDirExists := util.DirectoryExists(instanceDir) 116 | 117 | if !instanceDirExists { 118 | err := os.Mkdir(instanceDir, os.ModePerm) 119 | if err != nil { 120 | b.Logger.Criticalf("error creating instance directory: %s", err) 121 | 122 | return err 123 | } 124 | } 125 | 126 | il, err := instance.NewInstanceLoggersFOut(b.Logger, instanceDir) 127 | if err != nil { 128 | b.Logger.Criticalf("error instantiating loggers for instance: %s", err) 129 | 130 | return err 131 | } 132 | 133 | // snag the disk version the instance should be using, then update the in memory config value of 134 | // that disk to the instanceDir + "disk.qcow2" since that's what we name all boot disks. We do 135 | // this just for spawning the instance, we'll set it back to the diskVer after so that the 136 | // config file always shows the version that the instance was provisioned with. 137 | diskVer := b.Config.Instances[name].Disk 138 | instanceDisk := fmt.Sprintf("%s/disk.qcow2", instanceDir) 139 | b.Config.Instances[name].Disk = instanceDisk 140 | 141 | q, err := platforms.NewPlatformFromConfig( 142 | name, 143 | b.Config, 144 | il, 145 | ) 146 | if err != nil { 147 | b.Logger.Criticalf("error spawning instance from config: %s", err) 148 | 149 | return err 150 | } 151 | 152 | b.modifyInstanceMap(func() { b.Instances[name] = q }) 153 | 154 | // set the disk name back to the version for the config 155 | b.Config.Instances[name].Disk = diskVer 156 | 157 | err = b.startCheckDisk(name, instanceDisk) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | // now that we've sorted out the disk setup we can set the final/resolved disk on the in memory 163 | // instance setup 164 | b.Config.Instances[name].Disk = instanceDisk 165 | 166 | if b.Config.Instances[name].BootDelay > 0 { 167 | b.Logger.Infof("boot delay set, sleeping '%d' seconds", b.Config.Instances[name].BootDelay) 168 | 169 | time.Sleep(time.Duration(b.Config.Instances[name].BootDelay) * time.Second) 170 | } 171 | 172 | initialConfig := false 173 | 174 | if b.Config.Instances[name].StartupConfig != "" { 175 | initialConfig = true 176 | } 177 | 178 | err = q.Start(platforms.WithPrepareConsole(initialConfig)) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | if initialConfig { 184 | err = q.InstallConfig(b.Config.Instances[name].StartupConfig, true) 185 | if err != nil { 186 | return err 187 | } 188 | } 189 | 190 | b.Config.Instances[name].PID = q.GetPid() 191 | 192 | // reset the in memory config disk version back to the actual source disk version prior to 193 | // dumping the config to disk 194 | b.Config.Instances[name].Disk = diskVer 195 | 196 | err = b.Config.Dump(b.ConfigPath) 197 | if err != nil { 198 | b.Logger.Criticalf("error dumping updated boxen config to disk: %s", err) 199 | return err 200 | } 201 | 202 | b.Logger.Infof("start for instance '%s' completed successfully", name) 203 | 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /boxen/logging/instance.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const ( 12 | dequeInterval = 250 13 | timestampPlaces = 10 14 | debug = "debug" 15 | debugLevel = 10 16 | info = "info" 17 | infoLevel = 20 18 | critical = "critical" 19 | criticalLevel = 50 20 | ) 21 | 22 | var levelMap = map[string]int{ //nolint:gochecknoglobals 23 | debug: debugLevel, 24 | info: infoLevel, 25 | critical: criticalLevel, 26 | } 27 | 28 | var ErrLogError = errors.New("logError") 29 | 30 | // Instance represents an instance logging. In boxen there can and will be many logging 31 | // instances to hold logs for different instances or different processes or stdout vs stderr etc. 32 | type Instance struct { 33 | // Level is the level to log at, must be one of debug, info, or critical when setting, internal 34 | // value of level is assigned to the integer value of the log level. 35 | level int 36 | // Queue is a simple slice of *Message that is our logging queue. 37 | Queue *Queue 38 | // Logger is the logger provided by the user -- ex: log.Print. 39 | Logger func(...interface{}) 40 | // Formatter is the message formatter that provides encoding and decoding of messages. 41 | Formatter MessageFormatter 42 | wg *sync.WaitGroup 43 | done bool 44 | doneLock *sync.Mutex 45 | } 46 | 47 | // NewInstance returns a new logging Instance with locks and queue established. Eventually this 48 | // will accept options to set the Formatter and probably other things. 49 | func NewInstance(l func(...interface{}), opts ...Option) (*Instance, error) { 50 | li := &Instance{ 51 | level: infoLevel, 52 | Queue: newInstanceQueue(), 53 | Logger: l, 54 | Formatter: &DefaultFormatter{}, 55 | wg: &sync.WaitGroup{}, 56 | done: false, 57 | doneLock: &sync.Mutex{}, 58 | } 59 | 60 | Manager.addInstance(li) 61 | 62 | for _, o := range opts { 63 | err := o(li) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | return li, nil 70 | } 71 | 72 | // setDone safely sets the Instance done attribute. 73 | func (li *Instance) setDone(v bool) { 74 | li.doneLock.Lock() 75 | defer li.doneLock.Unlock() 76 | 77 | li.done = v 78 | } 79 | 80 | // getDone safely gets the Instance done attribute. 81 | func (li *Instance) getDone() bool { 82 | li.doneLock.Lock() 83 | defer li.doneLock.Unlock() 84 | 85 | return li.done 86 | } 87 | 88 | // buildMessage builds a Message from a string. 89 | func (li *Instance) buildMessage(l, f string) *Message { 90 | return &Message{ 91 | Message: f, 92 | Level: l, 93 | Timestamp: strconv.FormatInt(time.Now().Unix(), timestampPlaces), 94 | } 95 | } 96 | 97 | // queueMsg acquires a lock on the queue and places a Message into the queue before releasing the 98 | // lock. It also increments the queue depth. 99 | func (li *Instance) queueMsg(lm *Message) { 100 | li.Queue.lock.Lock() 101 | defer li.Queue.lock.Unlock() 102 | 103 | level := levelMap[lm.Level] 104 | 105 | if level >= li.level { 106 | li.Queue.queue = append(li.Queue.queue, lm) 107 | 108 | li.Queue.depth++ 109 | } 110 | } 111 | 112 | // deQueueMsg pops a message off the queue and sends it to the Logger. 113 | func (li *Instance) deQueueMsg() { 114 | li.Queue.lock.Lock() 115 | defer li.Queue.lock.Unlock() 116 | 117 | lm := li.Queue.queue[0] 118 | li.Queue.queue = li.Queue.queue[1:] 119 | 120 | li.Queue.depth-- 121 | 122 | li.Logger(li.Formatter.Encode(lm)) 123 | } 124 | 125 | // getQueueDepth returns the length of the log queue. 126 | func (li *Instance) getQueueDepth() int { 127 | li.Queue.lock.Lock() 128 | defer li.Queue.lock.Unlock() 129 | 130 | return li.Queue.depth 131 | } 132 | 133 | // Debug accepts a debug level log message with no formatting. 134 | func (li *Instance) Debug(f string) { 135 | li.queueMsg(li.buildMessage(debug, f)) 136 | } 137 | 138 | // Debugf accepts a debug level log message normal fmt.Sprintf type formatting. 139 | func (li *Instance) Debugf(f string, a ...interface{}) { 140 | li.queueMsg(li.buildMessage(debug, fmt.Sprintf(f, a...))) 141 | } 142 | 143 | // Info accepts an info level log message with no formatting. 144 | func (li *Instance) Info(f string) { 145 | li.queueMsg(li.buildMessage(info, f)) 146 | } 147 | 148 | // Infof accepts an info level log message normal fmt.Sprintf type formatting. 149 | func (li *Instance) Infof(f string, a ...interface{}) { 150 | li.queueMsg(li.buildMessage(info, fmt.Sprintf(f, a...))) 151 | } 152 | 153 | // Critical accepts a critical level log message with no formatting. 154 | func (li *Instance) Critical(f string) { 155 | li.queueMsg(li.buildMessage(critical, f)) 156 | } 157 | 158 | // Criticalf accepts a critical level log message normal fmt.Sprintf type formatting. 159 | func (li *Instance) Criticalf(f string, a ...interface{}) { 160 | li.queueMsg(li.buildMessage(critical, fmt.Sprintf(f, a...))) 161 | } 162 | 163 | // logb provides common functionality for Debugb Infob and Criticalb methods. 164 | func (li *Instance) logb(l string, b []byte) { 165 | li.wg.Add(1) 166 | 167 | go func() { 168 | for { 169 | if li.getDone() && len(b) == 0 { 170 | li.wg.Done() 171 | 172 | return 173 | } 174 | 175 | for len(b) > 0 { 176 | rb := b 177 | b = []byte{} 178 | 179 | li.queueMsg(li.buildMessage(l, string(rb))) 180 | } 181 | 182 | time.Sleep(dequeInterval * time.Millisecond) 183 | } 184 | }() 185 | } 186 | 187 | // Debugb accepts a debug level log message as a byte slice. 188 | func (li *Instance) Debugb(b []byte) { 189 | li.logb(debug, b) 190 | } 191 | 192 | // Infob accepts a debug level log message as a byte slice. 193 | func (li *Instance) Infob(b []byte) { 194 | li.logb(info, b) 195 | } 196 | 197 | // Criticalb accepts a debug level log message as a byte slice. 198 | func (li *Instance) Criticalb(b []byte) { 199 | li.logb(critical, b) 200 | } 201 | 202 | // Start starts the queue listener of the Instance. 203 | func (li *Instance) Start() { 204 | li.wg.Add(1) 205 | 206 | go func() { 207 | for { 208 | if li.getDone() && li.getQueueDepth() == 0 { 209 | li.Drain() 210 | li.wg.Done() 211 | 212 | return 213 | } 214 | 215 | for li.getQueueDepth() > 0 { 216 | li.deQueueMsg() 217 | } 218 | 219 | time.Sleep(dequeInterval * time.Millisecond) 220 | } 221 | }() 222 | } 223 | 224 | // Drain drains any remaining messages in the Instance Queue. 225 | func (li *Instance) Drain() { 226 | for li.getQueueDepth() > 0 { 227 | li.deQueueMsg() 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /boxen/platforms/factory.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | soptions "github.com/scrapli/scrapligo/driver/options" 8 | 9 | "github.com/carlmontanari/boxen/boxen/config" 10 | "github.com/carlmontanari/boxen/boxen/instance" 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | ) 13 | 14 | func GetPlatformType(v, p string) string { 15 | switch v { 16 | case VendorArista: 17 | if p == PlatformAristaVeos { 18 | return PlatformTypeAristaVeos 19 | } 20 | case VendorCisco: 21 | switch p { 22 | case PlatformCiscoCsr1000v: 23 | return PlatformTypeCiscoCsr1000v 24 | case PlatformCiscoXrv9k: 25 | return PlatformTypeCiscoXrv9k 26 | case PlatformCiscoN9kv: 27 | return PlatformTypeCiscoN9kv 28 | } 29 | case VendorJuniper: 30 | if p == PlatformJuniperVsrx { 31 | return PlatformTypeJuniperVsrx 32 | } 33 | case VendorPaloAlto: 34 | if p == PlatformPaloAltoPanos { 35 | return PlatformTypePaloAltoPanos 36 | } 37 | case VendorIPInfusion: 38 | if p == PlatformIPInfusionOcNOS { 39 | return PlatformTypeIPInfusionOcNOS 40 | } 41 | case VendorCheckpoint: 42 | if p == PlatformCheckpointCloudguard { 43 | return PlatformTypeCheckpointCloudguard 44 | } 45 | } 46 | 47 | return "" 48 | } 49 | 50 | func GetPlatformEmptyStruct(pT string) (Platform, error) { 51 | switch pT { 52 | case PlatformTypeAristaVeos: 53 | return &AristaVeos{}, nil 54 | case PlatformTypeCiscoCsr1000v: 55 | return &CiscoCsr1000v{}, nil 56 | case PlatformTypeCiscoXrv9k: 57 | return &CiscoXrv9k{}, nil 58 | case PlatformTypeCiscoN9kv: 59 | return &CiscoN9kv{}, nil 60 | case PlatformTypeJuniperVsrx: 61 | return &JuniperVsrx{}, nil 62 | case PlatformTypePaloAltoPanos: 63 | return &PaloAltoPanos{}, nil 64 | case PlatformTypeIPInfusionOcNOS: 65 | return &IPInfusionOcNOS{}, nil 66 | case PlatformTypeCheckpointCloudguard: 67 | return &CheckpointCloudguard{}, nil 68 | } 69 | 70 | return nil, fmt.Errorf( 71 | "%w: unknown platform type, this shouldn't happen", 72 | util.ErrValidationError, 73 | ) 74 | } 75 | 76 | // GetPlatformScrapliDefinition sets the scrapli platform definition to a value 77 | // of the BOXEN_SCRAPLI_PLATFORM_DEFINITION env var or to a default string value. 78 | func GetPlatformScrapliDefinition(p string) string { 79 | scrapliPlatform := os.Getenv("BOXEN_SCRAPLI_PLATFORM_DEFINITION") 80 | if scrapliPlatform != "" { 81 | return scrapliPlatform 82 | } 83 | 84 | // retrieve default scrapli platform url/name 85 | // when env var is not set 86 | switch p { 87 | case PlatformTypeAristaVeos: 88 | return AristaVeosScrapliPlatform 89 | case PlatformTypeCiscoCsr1000v: 90 | return CiscoCsr1000vScrapliPlatform 91 | case PlatformTypeCiscoXrv9k: 92 | return CiscoXrv9kScrapliPlatform 93 | case PlatformTypeCiscoN9kv: 94 | return CiscoN9kvScrapliPlatform 95 | case PlatformTypeJuniperVsrx: 96 | return JuniperVsrxScrapliPlatform 97 | case PlatformTypePaloAltoPanos: 98 | return PaloAltoPanosScrapliPlatform 99 | case PlatformTypeIPInfusionOcNOS: 100 | return IPInfusionOcNOSScrapliPlatform 101 | case PlatformTypeCheckpointCloudguard: 102 | return CheckpointCloudguardScrapliPlatform 103 | } 104 | 105 | return "" 106 | } 107 | 108 | func NewPlatformFromConfig( //nolint:funlen 109 | n string, 110 | c *config.Config, 111 | l *instance.Loggers, 112 | ) (Platform, error) { 113 | iCfg := c.Instances[n] 114 | pT := iCfg.PlatformType 115 | 116 | q, err := instance.NewQemu(n, c, l) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | var p Platform 122 | 123 | var con *ScrapliConsole 124 | 125 | scrapliPlatform := GetPlatformScrapliDefinition(pT) 126 | 127 | switch pT { 128 | case PlatformTypeAristaVeos: 129 | con, err = NewScrapliConsole( 130 | scrapliPlatform, 131 | q.Hardware.SerialPorts[0], 132 | q.Credentials.Username, 133 | q.Credentials.Password, 134 | l, 135 | ) 136 | 137 | p = &AristaVeos{ 138 | Qemu: q, 139 | ScrapliConsole: con, 140 | } 141 | case PlatformTypeCiscoCsr1000v: 142 | con, err = NewScrapliConsole( 143 | scrapliPlatform, 144 | q.Hardware.SerialPorts[0], 145 | q.Credentials.Username, 146 | q.Credentials.Password, 147 | l, 148 | ) 149 | 150 | p = &CiscoCsr1000v{ 151 | Qemu: q, 152 | ScrapliConsole: con, 153 | } 154 | case PlatformTypeCiscoXrv9k: 155 | con, err = NewScrapliConsole( 156 | scrapliPlatform, 157 | q.Hardware.SerialPorts[0], 158 | q.Credentials.Username, 159 | q.Credentials.Password, 160 | l, 161 | soptions.WithReturnChar("\r"), 162 | ) 163 | 164 | p = &CiscoXrv9k{ 165 | Qemu: q, 166 | ScrapliConsole: con, 167 | } 168 | case PlatformTypeCiscoN9kv: 169 | con, err = NewScrapliConsole( 170 | scrapliPlatform, 171 | q.Hardware.SerialPorts[0], 172 | q.Credentials.Username, 173 | q.Credentials.Password, 174 | l, 175 | soptions.WithReturnChar("\r"), 176 | ) 177 | 178 | p = &CiscoN9kv{ 179 | Qemu: q, 180 | ScrapliConsole: con, 181 | } 182 | case PlatformTypeJuniperVsrx: 183 | con, err = NewScrapliConsole( 184 | scrapliPlatform, 185 | q.Hardware.SerialPorts[0], 186 | q.Credentials.Username, 187 | q.Credentials.Password, 188 | l, 189 | ) 190 | 191 | p = &JuniperVsrx{ 192 | Qemu: q, 193 | ScrapliConsole: con, 194 | } 195 | case PlatformTypePaloAltoPanos: 196 | con, err = NewScrapliConsole( 197 | scrapliPlatform, 198 | q.Hardware.SerialPorts[0], 199 | q.Credentials.Username, 200 | q.Credentials.Password, 201 | l, 202 | soptions.WithReturnChar("\r"), 203 | ) 204 | 205 | p = &PaloAltoPanos{ 206 | Qemu: q, 207 | ScrapliConsole: con, 208 | } 209 | case PlatformTypeIPInfusionOcNOS: 210 | con, err = NewScrapliConsole( 211 | scrapliPlatform, 212 | q.Hardware.SerialPorts[0], 213 | q.Credentials.Username, 214 | q.Credentials.Password, 215 | l, 216 | soptions.WithReturnChar("\r"), 217 | ) 218 | 219 | p = &IPInfusionOcNOS{ 220 | Qemu: q, 221 | ScrapliConsole: con, 222 | } 223 | case PlatformTypeCheckpointCloudguard: 224 | con, err = NewScrapliConsole( 225 | scrapliPlatform, 226 | q.Hardware.SerialPorts[0], 227 | q.Credentials.Username, 228 | q.Credentials.Password, 229 | l, 230 | soptions.WithReturnChar("\r"), 231 | ) 232 | 233 | p = &CheckpointCloudguard{ 234 | Qemu: q, 235 | ScrapliConsole: con, 236 | } 237 | default: 238 | return nil, fmt.Errorf("%w: scrapligo driver is not found for %q platform", 239 | util.ErrAllocationError, pT) 240 | } 241 | 242 | return p, err 243 | } 244 | -------------------------------------------------------------------------------- /boxen/platforms/junipervsrx.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "time" 7 | 8 | sopoptions "github.com/scrapli/scrapligo/driver/opoptions" 9 | 10 | "github.com/carlmontanari/boxen/boxen/instance" 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | 13 | "github.com/scrapli/scrapligo/channel" 14 | ) 15 | 16 | const ( 17 | JuniperVsrxScrapliPlatform = "juniper_junos" 18 | ) 19 | 20 | type JuniperVsrx struct { 21 | *instance.Qemu 22 | *ScrapliConsole 23 | } 24 | 25 | func (p *JuniperVsrx) Package( 26 | sourceDir, packageDir string, 27 | ) (packageFiles, runFiles []string, err error) { 28 | _, _ = sourceDir, packageDir 29 | return []string{}, []string{}, err 30 | } 31 | 32 | func (p *JuniperVsrx) modifyStartCmd(c *instance.QemuLaunchCmd) { 33 | _ = c 34 | } 35 | 36 | func (p *JuniperVsrx) modifyInstallCmd(c *instance.QemuLaunchCmd) { 37 | _ = c 38 | } 39 | 40 | func (p *JuniperVsrx) startReady(install bool) error { 41 | err := p.openRetry() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = p.readUntil( 47 | []byte("login:"), 48 | getPlatformBootTimeout(PlatformJuniperVsrx), 49 | ) 50 | if err != nil || !install { 51 | return err 52 | } 53 | 54 | err = p.c.Channel.WriteAndReturn([]byte("root"), false) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | err = p.readUntil( 60 | // this read takes a while! (sometimes ~180s) 61 | []byte("root@%"), 62 | 180, //nolint:gomnd 63 | ) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = p.c.Channel.WriteAndReturn([]byte("cli"), false) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | return err 74 | } 75 | 76 | func (p *JuniperVsrx) Install(opts ...instance.Option) error { 77 | p.Loggers.Base.Info("install requested") 78 | 79 | a, opts, err := setInstallArgs(opts...) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | opts = append(opts, instance.WithLaunchModifier(p.modifyInstallCmd)) 85 | 86 | c := make(chan error, 1) 87 | stop := make(chan bool, 1) 88 | 89 | go func() { 90 | err = p.Qemu.Start(opts...) 91 | if err != nil { 92 | c <- err 93 | } 94 | 95 | p.Loggers.Base.Debug("instance started, waiting for start ready state") 96 | 97 | err = p.startReady(true) 98 | if err != nil { 99 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 100 | 101 | c <- err 102 | } 103 | 104 | p.Loggers.Base.Debug("start ready state acquired, logging in") 105 | 106 | err = p.login( 107 | &loginArgs{ 108 | username: string(p.c.Channel.ReturnChar), 109 | password: string(p.c.Channel.ReturnChar), 110 | }, 111 | ) 112 | if err != nil { 113 | c <- err 114 | } 115 | 116 | p.Loggers.Base.Debug("log in complete") 117 | 118 | if a.configLines != nil { 119 | p.Loggers.Base.Debug("install config lines provided, executing scrapligo on open") 120 | 121 | err = p.defOnOpen(p.c) 122 | if err != nil { 123 | p.Loggers.Base.Criticalf("error running scrapligo on open: %s\n", err) 124 | 125 | c <- err 126 | } 127 | 128 | err = p.Config( 129 | util.ConfigLinesMd5Password( 130 | a.configLines, 131 | regexp.MustCompile(`(?i)(?:set system .* encrypted-password )(.*$)`), 132 | ), 133 | ) 134 | if err != nil { 135 | p.Loggers.Base.Criticalf("error sending install config lines: %s\n", err) 136 | 137 | c <- err 138 | } 139 | } 140 | 141 | p.Loggers.Base.Debug("initial installation complete") 142 | 143 | err = p.SaveConfig() 144 | if err != nil { 145 | p.Loggers.Base.Criticalf("error saving config: %s\n", err) 146 | 147 | c <- err 148 | } 149 | 150 | // small delay ensuring config is saved nicely, without this extra sleep things just seem to 151 | // not actually "save" despite the "save complete" or whatever output. 152 | time.Sleep(5 * time.Second) // nolint:gomnd 153 | 154 | c <- nil 155 | stop <- true 156 | }() 157 | 158 | go p.WatchMainProc(c, stop) 159 | 160 | err = <-c 161 | if err != nil { 162 | return err 163 | } 164 | 165 | p.Loggers.Base.Info("install complete, stopping instance") 166 | 167 | return p.Stop(opts...) 168 | } 169 | 170 | func (p *JuniperVsrx) Start(opts ...instance.Option) error { //nolint:dupl 171 | p.Loggers.Base.Info("start platform instance requested") 172 | 173 | a, opts, err := setStartArgs(opts...) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | opts = append(opts, instance.WithLaunchModifier(p.modifyStartCmd)) 179 | 180 | err = p.Qemu.Start(opts...) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | err = p.startReady(false) 186 | if err != nil { 187 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 188 | 189 | return err 190 | } 191 | 192 | if !a.prepareConsole { 193 | p.Loggers.Base.Info("prepare console not requested, starting instance complete") 194 | 195 | return nil 196 | } 197 | 198 | err = p.login( 199 | &loginArgs{ 200 | username: p.Credentials.Username, 201 | password: p.Credentials.Password, 202 | }, 203 | ) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | err = p.defOnOpen(p.c) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | p.Loggers.Base.Info("starting platform instance complete") 214 | 215 | return nil 216 | } 217 | 218 | func (p *JuniperVsrx) SaveConfig() error { 219 | p.Loggers.Base.Info("save config requested") 220 | 221 | _, err := p.c.SendConfig( 222 | "commit", 223 | sopoptions.WithTimeoutOps( 224 | time.Duration(getPlatformSaveTimeout(PlatformJuniperVsrx))*time.Second, 225 | ), 226 | ) 227 | 228 | return err 229 | } 230 | 231 | func (p *JuniperVsrx) SetUserPass(usr, pwd string) error { 232 | p.Loggers.Base.Infof("set user/password for user '%s' requested", usr) 233 | 234 | _, err := p.c.SendInteractive( 235 | []*channel.SendInteractiveEvent{ 236 | { 237 | ChannelInput: fmt.Sprintf( 238 | "set system login user %s class super-user authentication plain-text-password", 239 | usr, 240 | ), 241 | ChannelResponse: "New password:", 242 | HideInput: false, 243 | }, 244 | { 245 | ChannelInput: pwd, 246 | ChannelResponse: "Retype new password:", 247 | HideInput: true, 248 | }, 249 | { 250 | ChannelInput: pwd, 251 | ChannelResponse: "#", 252 | HideInput: true, 253 | }, 254 | }, 255 | sopoptions.WithPrivilegeLevel("configuration"), 256 | ) 257 | 258 | return err 259 | } 260 | 261 | func (p *JuniperVsrx) SetHostname(h string) error { 262 | p.Loggers.Base.Infof("set hostname '%s' requested", h) 263 | 264 | return p.Config([]string{fmt.Sprintf( 265 | "set system host-name %s", 266 | h)}) 267 | } 268 | -------------------------------------------------------------------------------- /boxen/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/carlmontanari/boxen/boxen/util" 9 | 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | const ( 14 | // MAXINSTANCES is the maximum number of instances boxen can allocate. 15 | MAXINSTANCES = 255 16 | // MONITORPORTBASE is the starting port ID for qemu monitor ports. 17 | MONITORPORTBASE = 4000 18 | // SERIALPORTBASE is the starting port ID for serial ports. 19 | SERIALPORTBASE = 5000 20 | // SERIALPORTLOW is the first possible serial port ID. 21 | SERIALPORTLOW = 5001 22 | // SERIALPORTHI is the last possible serial port ID. 23 | SERIALPORTHI = 5999 24 | // MGMTNATLOW is the first possible "management NAT" port ID. 25 | MGMTNATLOW = 30001 26 | // MGMTNATHI is the last possible "management NAT" port ID. 27 | MGMTNATHI = 39999 28 | // SOCKETLISTENPORTLOW is the starting port ID for "data plane" listening ports. 29 | SOCKETLISTENPORTLOW = 40000 30 | // SOCKETLISTENPORTHI is the last possible port ID for "data plane" listening ports. 31 | SOCKETLISTENPORTHI = 65535 32 | ) 33 | 34 | // Config is a struct representing boxen configuration data. 35 | type Config struct { 36 | Options *GlobalOptions `yaml:"options,omitempty"` 37 | Instances map[string]*Instance `yaml:"instances,omitempty"` 38 | InstanceGroups map[string][]string `yaml:"groups,omitempty"` 39 | Platforms map[string]*Platform `yaml:"platforms,omitempty"` 40 | lock *sync.Mutex 41 | } 42 | 43 | // NewConfig returns a new instantiated config object. 44 | func NewConfig() *Config { 45 | return &Config{ 46 | &GlobalOptions{ 47 | Credentials: NewDefaultCredentials(), 48 | Qemu: &Qemu{ 49 | Acceleration: util.AvailableAccel(), 50 | Binary: util.GetQemuPath(), 51 | UseThickDisks: false, 52 | }, 53 | Build: &Build{ 54 | InstancePath: "", 55 | SourcePath: "", 56 | }, 57 | }, 58 | make(map[string]*Instance), 59 | make(map[string][]string), 60 | make(map[string]*Platform), 61 | &sync.Mutex{}, 62 | } 63 | } 64 | 65 | // NewPackageConfig returns a new instantiated config object specifically for use during "packaging" 66 | // this is mostly the same as normal, but omits the global config options (which include qemu path 67 | // and available acceleration). 68 | func NewPackageConfig() *Config { 69 | return &Config{ 70 | nil, 71 | make(map[string]*Instance), 72 | make(map[string][]string), 73 | make(map[string]*Platform), 74 | &sync.Mutex{}, 75 | } 76 | } 77 | 78 | func expandConfig(cfg *Config) { 79 | // ensure we don't have nil sections of the config 80 | if cfg.Options == nil { 81 | cfg.Options = &GlobalOptions{} 82 | cfg.Options.Credentials = NewDefaultCredentials() 83 | cfg.Options.Qemu = &Qemu{ 84 | Acceleration: util.AvailableAccel(), 85 | Binary: util.GetQemuPath(), 86 | UseThickDisks: false, 87 | } 88 | cfg.Options.Build = &Build{} 89 | } 90 | 91 | if cfg.Instances == nil { 92 | cfg.Instances = make(map[string]*Instance) 93 | } 94 | 95 | if cfg.InstanceGroups == nil { 96 | cfg.InstanceGroups = make(map[string][]string) 97 | } 98 | 99 | if cfg.Platforms == nil { 100 | cfg.Platforms = make(map[string]*Platform) 101 | } 102 | 103 | if cfg.lock == nil { 104 | cfg.lock = &sync.Mutex{} 105 | } 106 | } 107 | 108 | // NewConfigFromFile returns an instantiated Config object loaded from a YAML file. 109 | func NewConfigFromFile(f string) (*Config, error) { 110 | yamlFile, err := os.ReadFile(f) 111 | 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | cfg := &Config{} 117 | 118 | err = yaml.UnmarshalStrict(yamlFile, cfg) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | expandConfig(cfg) 124 | 125 | err = cfg.Validate() 126 | 127 | return cfg, err 128 | } 129 | 130 | func (c *Config) validateIDs() error { 131 | allocatedIDs := c.AllocatedInstanceIDs() 132 | 133 | uniqueIDs := util.IntSliceUniqify(allocatedIDs) 134 | 135 | if len(allocatedIDs) != len(uniqueIDs) { 136 | return fmt.Errorf("%w: one or more overlapping instance ids", util.ErrValidationError) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (c *Config) validateMonitorPorts() error { 143 | allocatedMonitorPorts := c.AllocatedMonitorPorts() 144 | 145 | uniqueMonitorPorts := util.IntSliceUniqify(allocatedMonitorPorts) 146 | 147 | if len(allocatedMonitorPorts) != len(uniqueMonitorPorts) { 148 | return fmt.Errorf("%w: one or more overlapping monitor ports", util.ErrValidationError) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (c *Config) validateSerialPorts() error { 155 | allocatedSerialPorts := c.AllocatedSerialPorts() 156 | 157 | uniqueSerialPorts := util.IntSliceUniqify(allocatedSerialPorts) 158 | 159 | if len(allocatedSerialPorts) != len(uniqueSerialPorts) { 160 | return fmt.Errorf("%w: one or more overlapping serial ports", util.ErrValidationError) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func (c *Config) validateNATPorts() error { 167 | allocatedNatPorts := c.AllocatedHostSideNatPorts() 168 | 169 | uniqueNatPorts := util.IntSliceUniqify(allocatedNatPorts) 170 | 171 | if len(allocatedNatPorts) != len(uniqueNatPorts) { 172 | return fmt.Errorf( 173 | "%w: one or more overlapping host side nat ports", 174 | util.ErrValidationError, 175 | ) 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (c *Config) validateListenPorts() error { 182 | allocatedListenPorts := c.AllocatedDataPlaneListenPorts() 183 | 184 | uniqueListenPorts := util.IntSliceUniqify(allocatedListenPorts) 185 | 186 | if len(allocatedListenPorts) != len(uniqueListenPorts) { 187 | return fmt.Errorf("%w: one or more overlapping host listen ports", util.ErrValidationError) 188 | } 189 | 190 | return nil 191 | } 192 | 193 | // Validate provides basic configuration validation -- checking for things like duplicate device IDs 194 | // and duplicate allocated ports. 195 | func (c *Config) Validate() error { 196 | for _, f := range []func() error{ 197 | c.validateIDs, 198 | c.validateMonitorPorts, 199 | c.validateSerialPorts, 200 | c.validateNATPorts, 201 | c.validateListenPorts} { 202 | err := f() 203 | if err != nil { 204 | return err 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // Dump the config to disk at path 'f'. 212 | func (c *Config) Dump(f string) error { 213 | c.lock.Lock() 214 | defer c.lock.Unlock() 215 | 216 | y, err := yaml.Marshal(c) 217 | 218 | if err != nil { 219 | return err 220 | } 221 | 222 | err = os.WriteFile(f, y, util.FilePerms) 223 | 224 | return err 225 | } 226 | 227 | // AddInstance safely (with a lock) adds an instance to the config object Instances map. 228 | func (c *Config) AddInstance(name string, instance *Instance) { 229 | c.lock.Lock() 230 | defer c.lock.Unlock() 231 | 232 | c.Instances[name] = instance 233 | } 234 | 235 | // DeleteInstance safely (with a lock) deletes an instance from the config object Instances map. 236 | func (c *Config) DeleteInstance(name string) { 237 | c.lock.Lock() 238 | defer c.lock.Unlock() 239 | 240 | delete(c.Instances, name) 241 | } 242 | -------------------------------------------------------------------------------- /boxen/platforms/ciscocsr1000v.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | sopoptions "github.com/scrapli/scrapligo/driver/opoptions" 10 | 11 | "github.com/carlmontanari/boxen/boxen/command" 12 | "github.com/carlmontanari/boxen/boxen/instance" 13 | "github.com/carlmontanari/boxen/boxen/util" 14 | ) 15 | 16 | const ( 17 | CiscoCsr1000vInstallCdromName = "config.iso" 18 | CiscoCsr1000vScrapliPlatform = "cisco_iosxe" 19 | CiscoCsr1000vDefaultUser = "admin" 20 | CiscoCsr1000vDefaultPass = "admin" 21 | ) 22 | 23 | type CiscoCsr1000v struct { 24 | *instance.Qemu 25 | *ScrapliConsole 26 | } 27 | 28 | func ciscoCsr1000vInstallConfig() []byte { 29 | return []byte( 30 | "platform console serial\r\n\r\n" + 31 | "do clear platform software vnic-if nvtable\r\n\r\n" + 32 | "do wr\r\n\n" + 33 | "do reload\r\n", 34 | ) 35 | } 36 | 37 | func (p *CiscoCsr1000v) Package( 38 | sourceDir, packageDir string, 39 | ) (packageFiles, runFiles []string, err error) { 40 | _ = sourceDir 41 | 42 | err = os.WriteFile( 43 | fmt.Sprintf("%s/%s", packageDir, "iosxe_config.txt"), 44 | ciscoCsr1000vInstallConfig(), 45 | util.FilePerms, 46 | ) 47 | if err != nil { 48 | return nil, nil, err 49 | } 50 | 51 | // binary to create iso files 52 | var genisoBinary string 53 | 54 | switch { 55 | case util.CommandExists(util.ISOBinary): 56 | genisoBinary = util.ISOBinary 57 | case util.CommandExists(util.DarwinISOBinary): 58 | genisoBinary = util.DarwinISOBinary 59 | } 60 | 61 | switch { 62 | // if genisoBinary was not detected - use docker 63 | case genisoBinary == "": 64 | _, err = command.Execute( 65 | util.DockerCmd, 66 | command.WithArgs( 67 | []string{ 68 | "run", 69 | "-v", 70 | packageDir + ":/work", 71 | "-w", 72 | packageDir, 73 | util.ISOBinaryContainer, 74 | "mkisofs", 75 | "-l", 76 | "-o", 77 | CiscoCsr1000vInstallCdromName, 78 | "iosxe_config.txt", 79 | }, 80 | ), 81 | command.WithWait(true), 82 | ) 83 | default: 84 | _, err = command.Execute( 85 | genisoBinary, 86 | command.WithArgs( 87 | []string{"-l", "-o", CiscoCsr1000vInstallCdromName, "iosxe_config.txt"}, 88 | ), 89 | command.WithWorkDir(packageDir), 90 | command.WithWait(true), 91 | ) 92 | } 93 | 94 | if err != nil { 95 | return nil, nil, err 96 | } 97 | 98 | return []string{CiscoCsr1000vInstallCdromName}, []string{}, err 99 | } 100 | 101 | func (p *CiscoCsr1000v) patchCmdCdrom(c *instance.QemuLaunchCmd) { 102 | diskDir := filepath.Dir(p.Disk) 103 | c.Extra = append( 104 | c.Extra, 105 | []string{"-cdrom", fmt.Sprintf("%s/%s", diskDir, CiscoCsr1000vInstallCdromName)}...) 106 | } 107 | 108 | func (p *CiscoCsr1000v) modifyStartCmd(c *instance.QemuLaunchCmd) { 109 | _ = c 110 | } 111 | 112 | func (p *CiscoCsr1000v) modifyInstallCmd(c *instance.QemuLaunchCmd) { 113 | p.modifyStartCmd(c) 114 | p.patchCmdCdrom(c) 115 | } 116 | 117 | func (p *CiscoCsr1000v) startReady() error { 118 | err := p.openRetry() 119 | if err != nil { 120 | return err 121 | } 122 | 123 | err = p.readUntil( 124 | []byte("Press RETURN to get started"), 125 | getPlatformBootTimeout(PlatformTypeCiscoCsr1000v), 126 | ) 127 | 128 | return err 129 | } 130 | 131 | func (p *CiscoCsr1000v) Install(opts ...instance.Option) error { 132 | p.Loggers.Base.Info("install requested") 133 | 134 | a, opts, err := setInstallArgs(opts...) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | opts = append(opts, instance.WithLaunchModifier(p.modifyInstallCmd)) 140 | 141 | c := make(chan error, 1) 142 | stop := make(chan bool, 1) 143 | 144 | go func() { // nolint:dupl 145 | err = p.Qemu.Start(opts...) 146 | if err != nil { 147 | c <- err 148 | } 149 | 150 | p.Loggers.Base.Debug("instance started, waiting for start ready state") 151 | 152 | err = p.startReady() 153 | if err != nil { 154 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 155 | 156 | c <- err 157 | } 158 | 159 | p.Loggers.Base.Debug("start ready state acquired, logging in") 160 | 161 | err = p.login( 162 | &loginArgs{ 163 | username: CiscoCsr1000vDefaultUser, 164 | password: CiscoCsr1000vDefaultPass, 165 | }, 166 | ) 167 | if err != nil { 168 | c <- err 169 | } 170 | 171 | p.Loggers.Base.Debug("log in complete") 172 | 173 | if a.configLines != nil { 174 | p.Loggers.Base.Debug("install config lines provided, executing scrapligo on open") 175 | 176 | err = p.defOnOpen(p.c) 177 | if err != nil { 178 | p.Loggers.Base.Criticalf("error running scrapligo on open: %s\n", err) 179 | 180 | c <- err 181 | } 182 | 183 | err = p.Config(a.configLines) 184 | if err != nil { 185 | p.Loggers.Base.Criticalf("error sending install config lines: %s\n", err) 186 | 187 | c <- err 188 | } 189 | } 190 | 191 | p.Loggers.Base.Debug("initial installation complete") 192 | 193 | err = p.SaveConfig() 194 | if err != nil { 195 | p.Loggers.Base.Criticalf("error saving config: %s\n", err) 196 | 197 | c <- err 198 | } 199 | 200 | // small delay ensuring config is saved nicely, without this extra sleep things just seem to 201 | // not actually "save" despite the "save complete" or whatever output. 202 | time.Sleep(5 * time.Second) // nolint:gomnd 203 | 204 | c <- nil 205 | stop <- true 206 | }() 207 | 208 | go p.WatchMainProc(c, stop) 209 | 210 | err = <-c 211 | if err != nil { 212 | return err 213 | } 214 | 215 | p.Loggers.Base.Info("install complete, stopping instance") 216 | 217 | return p.Stop(opts...) 218 | } 219 | 220 | func (p *CiscoCsr1000v) Start(opts ...instance.Option) error { 221 | p.Loggers.Base.Info("start platform instance requested") 222 | 223 | a, opts, err := setStartArgs(opts...) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | opts = append(opts, instance.WithLaunchModifier(p.modifyStartCmd)) 229 | 230 | err = p.Qemu.Start(opts...) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | err = p.startReady() 236 | if err != nil { 237 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 238 | 239 | return err 240 | } 241 | 242 | if !a.prepareConsole { 243 | p.Loggers.Base.Info("prepare console not requested, starting instance complete") 244 | 245 | return nil 246 | } 247 | 248 | err = p.login( 249 | &loginArgs{ 250 | username: p.Credentials.Username, 251 | password: p.Credentials.Password, 252 | }, 253 | ) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | err = p.defOnOpen(p.c) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | p.Loggers.Base.Info("starting platform instance complete") 264 | 265 | return nil 266 | } 267 | 268 | func (p *CiscoCsr1000v) SaveConfig() error { 269 | p.Loggers.Base.Info("save config requested") 270 | 271 | _, err := p.c.SendCommand( 272 | "copy running-config startup-config", 273 | sopoptions.WithTimeoutOps( 274 | time.Duration(getPlatformSaveTimeout(PlatformTypeCiscoCsr1000v))*time.Second, 275 | ), 276 | ) 277 | 278 | return err 279 | } 280 | 281 | func (p *CiscoCsr1000v) SetUserPass(usr, pwd string) error { 282 | p.Loggers.Base.Infof("set user/password for user '%s' requested", usr) 283 | 284 | return p.Config([]string{fmt.Sprintf( 285 | "username %s privilege 15 password %s", 286 | usr, 287 | pwd)}) 288 | } 289 | 290 | func (p *CiscoCsr1000v) SetHostname(h string) error { 291 | p.Loggers.Base.Infof("set hostname '%s' requested", h) 292 | 293 | return p.Config([]string{fmt.Sprintf( 294 | "hostname %s", 295 | h)}) 296 | } 297 | -------------------------------------------------------------------------------- /boxen/platforms/aristaveos.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "path/filepath" 7 | "regexp" 8 | "time" 9 | 10 | "github.com/carlmontanari/boxen/boxen/instance" 11 | "github.com/carlmontanari/boxen/boxen/util" 12 | 13 | sopoptions "github.com/scrapli/scrapligo/driver/opoptions" 14 | ) 15 | 16 | const ( 17 | AristaVeosAbootFileName = "Aboot-veos-serial-8.0.0.iso" 18 | AristaVeosScrapliPlatform = "arista_eos" 19 | AristaVeosDefaultUser = "admin" 20 | AristaVeosDefaultPass = "admin" 21 | ) 22 | 23 | type AristaVeos struct { 24 | *instance.Qemu 25 | *ScrapliConsole 26 | } 27 | 28 | func (p *AristaVeos) Package( 29 | sourceDir, packageDir string, 30 | ) (packageFiles, runFiles []string, err error) { 31 | if !util.FileExists(fmt.Sprintf("%s/%s", sourceDir, AristaVeosAbootFileName)) { 32 | return nil, nil, fmt.Errorf( 33 | "%w: did not find Aboot iso in dir '%s'", 34 | util.ErrInspectionError, 35 | sourceDir, 36 | ) 37 | } 38 | 39 | err = util.CopyFile( 40 | fmt.Sprintf("%s/%s", sourceDir, AristaVeosAbootFileName), 41 | fmt.Sprintf("%s/%s", packageDir, AristaVeosAbootFileName), 42 | ) 43 | 44 | return []string{AristaVeosAbootFileName}, []string{}, err 45 | } 46 | 47 | func (p *AristaVeos) patchCmdMgmtNic(c *instance.QemuLaunchCmd) { 48 | // vEOS wants the mgmt port to be the first port on the bus, so make that happen... 49 | c.MgmtNic[1] += fmt.Sprintf( 50 | ",bus=pci.1,addr=0x%x", 51 | 2, //nolint:gomnd 52 | ) 53 | } 54 | 55 | func (p *AristaVeos) patchCmdDataNic(c *instance.QemuLaunchCmd) { 56 | var nicCmd []string 57 | 58 | for nicID := 1; nicID < p.Hardware.NicCount+1; nicID++ { 59 | // need to offset pci bus addr by one to account for mgmt nic 60 | busID := int(math.Floor(float64(nicID+1)/float64(p.Hardware.NicPerBus))) + 1 61 | busAddr := (nicID + 1%p.Hardware.NicPerBus) + 1 62 | paddedNicID := fmt.Sprintf("%03d", nicID) 63 | 64 | nicCmd = append( 65 | nicCmd, 66 | p.BuildDataNic(nicID, busID, busAddr, paddedNicID)...) 67 | } 68 | 69 | c.DataNic = nicCmd 70 | } 71 | 72 | func (p *AristaVeos) patchCmdCdrom(c *instance.QemuLaunchCmd) { 73 | diskDir := filepath.Dir(p.Disk) 74 | c.Extra = append( 75 | c.Extra, 76 | []string{"-cdrom", fmt.Sprintf("%s/%s", diskDir, AristaVeosAbootFileName)}...) 77 | } 78 | 79 | func (p *AristaVeos) modifyStartCmd(c *instance.QemuLaunchCmd) { 80 | p.patchCmdMgmtNic(c) 81 | p.patchCmdDataNic(c) 82 | } 83 | 84 | func (p *AristaVeos) modifyInstallCmd(c *instance.QemuLaunchCmd) { 85 | p.modifyStartCmd(c) 86 | p.patchCmdCdrom(c) 87 | } 88 | 89 | func (p *AristaVeos) startReady(install bool) error { 90 | err := p.openRetry() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if install { 96 | err = p.readUntil( 97 | // readUntil makes everything lower so we dont actually care about the case, but w/e 98 | []byte("localhost ZeroTouch:"), 99 | getPlatformBootTimeout(PlatformTypeAristaVeos), 100 | ) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | err = p.login( 106 | &loginArgs{ 107 | username: AristaVeosDefaultUser, 108 | password: AristaVeosDefaultPass, 109 | loginPattern: regexp.MustCompile(`(?i)localhost zerotouch:\s?`), 110 | }, 111 | ) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | // device auto reloads after this 117 | p.Loggers.Base.Debug("disabling zerotouch, device will reload") 118 | 119 | _ = p.c.Channel.WriteAndReturn([]byte("zerotouch disable"), false) 120 | } 121 | 122 | err = p.readUntil( 123 | []byte("login:"), 124 | getPlatformBootTimeout(PlatformTypeAristaVeos), 125 | ) 126 | 127 | return err 128 | } 129 | 130 | func (p *AristaVeos) Install(opts ...instance.Option) error { //nolint: funlen 131 | p.Loggers.Base.Info("install requested") 132 | 133 | a, opts, err := setInstallArgs(opts...) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | opts = append(opts, instance.WithLaunchModifier(p.modifyInstallCmd)) 139 | 140 | c := make(chan error, 1) 141 | stop := make(chan bool, 1) 142 | 143 | go func() { 144 | err = p.Qemu.Start(opts...) 145 | if err != nil { 146 | c <- err 147 | 148 | return 149 | } 150 | 151 | p.Loggers.Base.Debug("instance started, waiting for start ready state") 152 | 153 | err = p.startReady(true) 154 | if err != nil { 155 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 156 | 157 | c <- err 158 | 159 | return 160 | } 161 | 162 | p.Loggers.Base.Debug("start ready state acquired, logging in") 163 | 164 | err = p.login( 165 | &loginArgs{ 166 | username: AristaVeosDefaultUser, 167 | password: AristaVeosDefaultPass, 168 | }, 169 | ) 170 | if err != nil { 171 | c <- err 172 | 173 | return 174 | } 175 | 176 | p.Loggers.Base.Debug("log in complete") 177 | 178 | if a.configLines != nil { 179 | p.Loggers.Base.Debug("install config lines provided, executing scrapligo on open") 180 | 181 | err = p.defOnOpen(p.c) 182 | if err != nil { 183 | p.Loggers.Base.Criticalf("error running scrapligo on open: %s\n", err) 184 | 185 | c <- err 186 | 187 | return 188 | } 189 | 190 | err = p.Config(a.configLines) 191 | if err != nil { 192 | p.Loggers.Base.Criticalf("error sending install config lines: %s\n", err) 193 | 194 | c <- err 195 | 196 | return 197 | } 198 | } 199 | 200 | p.Loggers.Base.Debug("initial installation complete") 201 | 202 | err = p.SaveConfig() 203 | if err != nil { 204 | p.Loggers.Base.Criticalf("error saving config: %s\n", err) 205 | 206 | c <- err 207 | 208 | return 209 | } 210 | 211 | // small delay ensuring config is saved nicely, without this extra sleep things just seem to 212 | // not actually "save" despite the "save complete" or whatever output. 213 | time.Sleep(5 * time.Second) // nolint:gomnd 214 | 215 | c <- nil 216 | stop <- true 217 | }() 218 | 219 | go p.WatchMainProc(c, stop) 220 | 221 | err = <-c 222 | if err != nil { 223 | return err 224 | } 225 | 226 | p.Loggers.Base.Info("install complete, stopping instance") 227 | 228 | return p.Stop(opts...) 229 | } 230 | 231 | func (p *AristaVeos) Start(opts ...instance.Option) error { //nolint:dupl 232 | p.Loggers.Base.Info("start platform instance requested") 233 | 234 | a, opts, err := setStartArgs(opts...) 235 | if err != nil { 236 | return err 237 | } 238 | 239 | opts = append(opts, instance.WithLaunchModifier(p.modifyStartCmd)) 240 | 241 | err = p.Qemu.Start(opts...) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | err = p.startReady(false) 247 | if err != nil { 248 | p.Loggers.Base.Criticalf("error waiting for start ready state: %s\n", err) 249 | 250 | return err 251 | } 252 | 253 | if !a.prepareConsole { 254 | p.Loggers.Base.Info("prepare console not requested, starting instance complete") 255 | 256 | return nil 257 | } 258 | 259 | err = p.login( 260 | &loginArgs{ 261 | username: p.Credentials.Username, 262 | password: p.Credentials.Password, 263 | }, 264 | ) 265 | if err != nil { 266 | return err 267 | } 268 | 269 | err = p.defOnOpen(p.c) 270 | if err != nil { 271 | return err 272 | } 273 | 274 | p.Loggers.Base.Info("starting platform instance complete") 275 | 276 | return nil 277 | } 278 | 279 | func (p *AristaVeos) SaveConfig() error { 280 | p.Loggers.Base.Info("save config requested") 281 | 282 | _, err := p.c.SendCommand( 283 | "copy running-config startup-config", 284 | sopoptions.WithTimeoutOps( 285 | time.Duration(getPlatformSaveTimeout(PlatformTypeAristaVeos))*time.Second, 286 | ), 287 | ) 288 | 289 | return err 290 | } 291 | 292 | func (p *AristaVeos) SetUserPass(usr, pwd string) error { 293 | p.Loggers.Base.Infof("set user/password for user '%s' requested", usr) 294 | 295 | return p.Config([]string{fmt.Sprintf( 296 | "username %s secret 0 %s role network-admin", 297 | usr, 298 | pwd)}) 299 | } 300 | 301 | func (p *AristaVeos) SetHostname(h string) error { 302 | p.Loggers.Base.Infof("set hostname '%s' requested", h) 303 | 304 | return p.Config([]string{fmt.Sprintf( 305 | "hostname %s", 306 | h)}) 307 | } 308 | --------------------------------------------------------------------------------