├── .gitignore ├── LICENSE ├── README.md ├── cmd └── virgo │ └── main.go ├── pkg ├── depcheck │ ├── depcheck.go │ └── depcheck_test.go ├── network │ ├── network.go │ └── network_test.go ├── project │ ├── project.go │ ├── projects.go │ ├── projects_test.go │ └── pull.go ├── registry │ ├── registry.go │ └── registry_test.go ├── runner │ ├── runner.go │ └── runner_test.go └── tools │ ├── logo.go │ └── tools.go └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | rulez 2 | cmd/virgo/virgo 3 | virgo.yml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Defer Panic 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virgo 2 | 3 | ## This Repo is Deprecated 4 | 5 | But wait there's more! 6 | 7 | If you goto [github.com/nanovms/ops](https://github.com/nanovms/ops) 8 | right now you'll find a brand spanking new unikernel 9 | compiler/orchestrator built for the Nanos unikernel. It allows you to 10 | run arbitrary linux binaries - check it out. 11 | -------------------------------------------------------------------------------- /cmd/virgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "runtime" 8 | "text/tabwriter" 9 | 10 | "github.com/deferpanic/dpcli/api" 11 | "github.com/deferpanic/virgo/pkg/depcheck" 12 | "github.com/deferpanic/virgo/pkg/network" 13 | "github.com/deferpanic/virgo/pkg/project" 14 | "github.com/deferpanic/virgo/pkg/registry" 15 | "github.com/deferpanic/virgo/pkg/runner" 16 | "github.com/deferpanic/virgo/pkg/tools" 17 | 18 | "gopkg.in/alecthomas/kingpin.v2" 19 | ) 20 | 21 | var ( 22 | token string 23 | hostOS string 24 | 25 | app = kingpin.New("virgo", "Run Unikernels Locally") 26 | dry = app.Flag("dry", "dry run, print commands only").Short('n').Bool() 27 | 28 | pullCommand = app.Command("pull", "Pull a project") 29 | pullProjectName = pullCommand.Arg("name", "Project name.").Required().String() 30 | 31 | runCmd = app.Command("run", "Run a project") 32 | runHeadless = runCmd.Flag("headless", "Run project headless").Bool() 33 | runProjectName = runCmd.Arg("name", "Project name.").Required().String() 34 | 35 | killCommand = app.Command("kill", "Kill a running project") 36 | killProjectName = killCommand.Arg("name", "Project name.").Required().String() 37 | 38 | rmCommand = app.Command("rm", "Remove a project") 39 | rmProjectName = rmCommand.Arg("name", "Project name.").Required().String() 40 | 41 | logCommand = app.Command("log", "Fetch log of project") 42 | logProjectName = logCommand.Arg("name", "Project name.").Required().String() 43 | 44 | searchCommand = app.Command("search", "Search for a project") 45 | searchCommandName = searchCommand.Arg("description", "Description").Required().String() 46 | searchCommandStars = searchCommand.Arg("stars", "Star Count").Int() 47 | 48 | signupCommand = app.Command("signup", "Signup") 49 | signupEmail = signupCommand.Arg("email", "Email.").Required().String() 50 | signupUsername = signupCommand.Arg("username", "Username.").Required().String() 51 | signupPassword = signupCommand.Arg("password", "Password.").Required().String() 52 | 53 | psCommand = app.Command("ps", "List running projects") 54 | 55 | listCommand = app.Command("list", "List all projects") 56 | listJson = listCommand.Flag("json", "output as json").Bool() 57 | ) 58 | 59 | func main() { 60 | var ( 61 | stdout, stderr *os.File = os.Stdout, os.Stderr 62 | process runner.Runner 63 | ) 64 | 65 | log.SetFlags(log.Lshortfile) 66 | 67 | if len(os.Args) < 2 { 68 | fmt.Println(tools.Logo) 69 | } 70 | 71 | if len(os.Args) > 1 && os.Args[1] == "signup" { 72 | api.Cli = api.NewCliImplementation("") 73 | } else { 74 | if err := tools.SetToken(); err != nil { 75 | log.Fatalf("%s\nif you have and account add your token to '~/.dprc' otherwise signup via\nvirgo signup my@email.com username password", err) 76 | } 77 | } 78 | 79 | command := kingpin.MustParse(app.Parse(os.Args[1:])) 80 | 81 | if *dry { 82 | process = runner.NewDryRunner(stdout) 83 | } else { 84 | process = runner.NewExecRunner(stdout, stderr, false) 85 | } 86 | 87 | dep := depcheck.New(process) 88 | 89 | if command == "run" && runtime.GOOS == "darwin" { 90 | var err error 91 | log.Println("setting sysctl") 92 | 93 | if _, err := process.Shell("sysctl -w net.inet.ip.forwarding=1"); err != nil { 94 | log.Fatalf("Error enabling ip forwarding - %s", err) 95 | } 96 | 97 | if _, err = process.Shell("sysctl -w net.link.ether.inet.proxyall=1"); err != nil { 98 | log.Fatalf("error enabling proxyall - %s", err) 99 | } 100 | 101 | // enable this for lower osx versions 102 | version, err := dep.OsCheck() 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | if dep.IsNeedFw(version) { 107 | if _, err = process.Shell("sysctl -w net.inet.ip.fw.enable=1"); err != nil { 108 | log.Fatalf("error enabling ip firewall - %s", err) 109 | } 110 | } 111 | } 112 | 113 | r, err := registry.New() 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | 118 | projects, err := project.LoadProjects(r) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | killProject := func() { 124 | rt := projects.GetProjectByName(*killProjectName) 125 | if rt == nil { 126 | log.Fatalf("Project '%s' isn't running\n", *killProjectName) 127 | } 128 | 129 | for _, instance := range rt.Process { 130 | instance.Stop() 131 | } 132 | 133 | if err := projects.Delete(rt, r); err != nil { 134 | log.Fatal(err) 135 | } 136 | } 137 | 138 | switch command { 139 | case "pull": 140 | pr, err := r.AddProject(*pullProjectName) 141 | if err != nil { 142 | log.Fatal(err) 143 | } 144 | 145 | if err := project.Pull(pr); err != nil { 146 | log.Fatal(err) 147 | } 148 | 149 | case "run": 150 | pr := r.Project(*runProjectName) 151 | if pr.Name() == "" { 152 | log.Fatalf("Project '%s' not found\n", *runProjectName) 153 | } 154 | 155 | ip, gw := projects.GetNextNetowrk() 156 | if ip == "" || gw == "" { 157 | log.Fatal("Ip range is exceeded, unable to proceed") 158 | } 159 | 160 | network, err := network.New(pr, ip, gw) 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | fmt.Println("NextNum:", projects.NextNum()) 165 | p, err := project.New(pr, network, process, projects.NextNum()) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | 170 | if err = p.Run(*runHeadless); err != nil { 171 | log.Fatal(err) 172 | } 173 | 174 | if err := projects.Add(p, r); err != nil { 175 | log.Fatal(err) 176 | } 177 | 178 | fmt.Println() 179 | 180 | case "ps": 181 | result := projects.String() 182 | if result == "" { 183 | fmt.Fprintf(os.Stdout, "No projects running\n") 184 | } 185 | 186 | w := tabwriter.NewWriter(os.Stdout, 4, 8, 2, '\t', 0) 187 | fmt.Fprintf(w, "%s", projects) 188 | w.Flush() 189 | 190 | case "kill": 191 | killProject() 192 | 193 | case "rm": 194 | killProject() 195 | 196 | if err := r.PurgeProject(*rmProjectName); err != nil { 197 | log.Fatal(err) 198 | } 199 | 200 | case "log": 201 | pr, err := r.AddProject(*logProjectName) 202 | if err != nil { 203 | log.Fatal(err) 204 | } 205 | 206 | if err := tools.ShowFiles(pr.LogsDir()); err != nil { 207 | log.Fatal(err) 208 | } 209 | 210 | case "search": 211 | search := &api.Search{} 212 | if *searchCommandStars != 0 { 213 | search.FindWithStars(*searchCommandName, *searchCommandStars) 214 | } else { 215 | search.Find(*searchCommandName) 216 | } 217 | 218 | case "signup": 219 | users := &api.Users{} 220 | users.Create(*signupEmail, *signupUsername, *signupPassword) 221 | 222 | case "list": 223 | var running bool 224 | 225 | list := r.ProjectList() 226 | if len(list) == 0 { 227 | fmt.Fprintf(os.Stdout, "No projects found\n") 228 | } 229 | 230 | w := tabwriter.NewWriter(os.Stdout, 1, 8, 0, '\t', 0) 231 | 232 | fmt.Fprintf(w, "Project name\tRunning\n") 233 | fmt.Fprintf(w, "------------\t-------\n") 234 | 235 | for _, item := range list { 236 | running = false 237 | 238 | if p := projects.GetProjectByName(item.Name()); p != nil { 239 | if len(p.Process) > 0 { 240 | running = true 241 | } 242 | } 243 | 244 | fmt.Fprintf(w, "%s\t%v\n", item.Name(), running) 245 | } 246 | 247 | w.Flush() 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /pkg/depcheck/depcheck.go: -------------------------------------------------------------------------------- 1 | package depcheck 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/deferpanic/virgo/pkg/runner" 9 | ) 10 | 11 | // supportedDarwin contains the list of known osx versions that work 12 | const minDarwinSupported = "10.11.4" 13 | 14 | // darwinFW contains the known list of osx versions that need the 15 | // fw.enable sysctl setting 16 | var darwinFW = []string{"10.11.4", "10.11.5", "10.11.6"} 17 | 18 | type DepCehck struct { 19 | r runner.Runner 20 | } 21 | 22 | func New(r runner.Runner) DepCehck { 23 | return DepCehck{ 24 | r: r, 25 | } 26 | } 27 | 28 | func (d DepCehck) RunAll() error { 29 | if _, err := d.OsCheck(); err != nil { 30 | return err 31 | } 32 | 33 | if !d.HasQemu() { 34 | return fmt.Errorf("QEMU not found\nYou can install it\n- via homebrew: brew install qemu\n- via port: port install qemu\n- manually: https://www.qemu.org/download/#source") 35 | } 36 | 37 | if !d.HasCpulimit() { 38 | return fmt.Errorf("cpulimit not found\nYou can install it\n- via howbrew: brew install cpulimit\n- via port: port install cpulimit\n- manually: https://github.com/opsengine/cpulimit") 39 | } 40 | 41 | if !d.HasTunTap() { 42 | return fmt.Errorf("tuntap not found\nPlease download and install tuntaposx\nhttp://downloads.sourceforge.net/tuntaposx/tuntap_20150118.tar.gz") 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (d DepCehck) HasHAX() bool { 49 | if _, err := d.r.Shell("kextstat | grep -c hax"); err != nil { 50 | return false 51 | } 52 | 53 | return true 54 | } 55 | 56 | func (d DepCehck) HasCpulimit() bool { 57 | if _, err := d.r.Shell("which cpulimit"); err != nil { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | 64 | func (d DepCehck) HasQemu() bool { 65 | if _, err := d.r.Shell("which qemu-system-x86_64"); err != nil { 66 | return false 67 | } 68 | 69 | return true 70 | } 71 | 72 | func (d DepCehck) HasTunTap() bool { 73 | if _, err := d.r.Shell("kextstat | grep -c tuntap"); err != nil { 74 | return false 75 | } 76 | 77 | return true 78 | } 79 | 80 | func (d DepCehck) IsNeedFw(ver string) bool { 81 | for i := 0; i < len(darwinFW); i++ { 82 | if darwinFW[i] == ver { 83 | return true 84 | } 85 | } 86 | 87 | return false 88 | } 89 | 90 | func (d DepCehck) OsCheck() (string, error) { 91 | out, err := d.r.Shell("sw_vers -productVersion") 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | // hack for dry-run mode 97 | if _, ok := d.r.(runner.DryRunner); ok { 98 | out = []byte(minDarwinSupported) 99 | } 100 | 101 | version := strings.TrimSpace(string(out)) 102 | 103 | // Check if we're above or equal to the minimum Darwin version 104 | if IsValidDarwin(version) { 105 | return version, nil 106 | } 107 | 108 | return "", fmt.Errorf("You are running OS X version %s\nThis application is only supports OS X version %s or higher\npf_ctl is used. If using an earlier osx you might need to use natd or contribute a patch.\n", version, minDarwinSupported) 109 | } 110 | 111 | func getVersionParts(ver string) []int { 112 | var parts []int 113 | for _, part := range strings.Split(ver, ".") { 114 | partInt, err := strconv.Atoi(part) 115 | if err != nil { 116 | return []int{} 117 | } 118 | parts = append(parts, partInt) 119 | } 120 | 121 | // Normalize to x.y.z version parts 122 | switch len(parts) { 123 | case 2: 124 | parts = append(parts, 0) 125 | case 1: 126 | parts = append(parts, 0, 0) 127 | } 128 | 129 | return parts 130 | } 131 | 132 | // IsValidDarwin returns whether or not you are using a supported Darwin version. 133 | func IsValidDarwin(ver string) bool { 134 | userVersionParts := getVersionParts(ver) 135 | minVersionParts := getVersionParts(minDarwinSupported) 136 | 137 | // Ensure versions are correctly formatted 138 | if len(minVersionParts) != 3 || len(userVersionParts) != 3 { 139 | return false 140 | } 141 | 142 | switch { 143 | // Below supported major version 144 | case userVersionParts[0] < minVersionParts[0]: 145 | return false 146 | 147 | // Above supported major version 148 | case userVersionParts[0] > minVersionParts[0]: 149 | return true 150 | 151 | // Check for for minor/patch versions. 152 | default: 153 | switch { 154 | // Below minimum minor for minimum major. 155 | case userVersionParts[1] < minVersionParts[1]: 156 | return false 157 | 158 | // Above minimum minor for minimum major. 159 | case userVersionParts[1] > minVersionParts[1]: 160 | return true 161 | 162 | // If on lowest major and minor version, make sure at least on latest patch 163 | default: 164 | if userVersionParts[2] >= minVersionParts[2] { 165 | return true 166 | } 167 | 168 | // Below latest patch 169 | return false 170 | 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pkg/depcheck/depcheck_test.go: -------------------------------------------------------------------------------- 1 | package depcheck 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsValidDarwin(t *testing.T) { 8 | tt := []struct { 9 | Version string 10 | Validity bool 11 | }{ 12 | 13 | {"10.11.3", false}, 14 | {"9.1.3", false}, 15 | {"7.1.3", false}, 16 | {"6.1.2", false}, 17 | {"", false}, 18 | {"9", false}, 19 | {"9.12", false}, 20 | 21 | {"11.11.4", true}, 22 | {"10.11.5", true}, 23 | {"10.11.6", true}, 24 | {"10.12", true}, 25 | {"11", true}, 26 | {"10.12.2", true}, 27 | {"10.12.3", true}, 28 | {"10.12.6", true}, 29 | {"10.13.1", true}, 30 | {"10.13.3", true}, 31 | {"10.14.0", true}, 32 | } 33 | 34 | for _, tc := range tt { 35 | v := IsValidDarwin(tc.Version) 36 | if v != tc.Validity { 37 | t.Errorf("Exepcted IsValidDarwin(\"%s\") to be %v, but was %v", tc.Version, tc.Validity, v) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "os" 7 | "text/template" 8 | 9 | "github.com/deferpanic/virgo/pkg/registry" 10 | ) 11 | 12 | type Network struct { 13 | Gw string 14 | Ip string 15 | Mac string 16 | } 17 | 18 | var ifupTpl = template.Must(template.New("").Parse(`#!/bin/bash 19 | sudo ifconfig $1 {{ .Gw }} netmask 255.255.255.0 up 20 | 21 | unamestr=` + "`uname`" + ` 22 | if [[ "$unamestr" == 'Darwin' ]]; then 23 | sudo pfctl -d 24 | echo "nat on en0 from $1:network to any -> (en0)" > rulez 25 | sudo pfctl -f ./rulez -e 26 | fi 27 | `)) 28 | 29 | var ifdownTpl = template.Must(template.New("").Parse(`#!/bin/bash 30 | ifconfig $1 down 31 | `)) 32 | 33 | func New(p registry.Project, ip, gw string) (Network, error) { 34 | if ip == "" || gw == "" { 35 | return Network{}, fmt.Errorf("ip and gw can't be empty") 36 | } 37 | 38 | network := Network{ 39 | Gw: gw, 40 | Ip: ip, 41 | Mac: (Network{}).generateMAC(), 42 | } 43 | 44 | wr, err := os.OpenFile(p.IfUpFile(), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) 45 | if err != nil { 46 | return Network{}, fmt.Errorf("error creating %s file - %s\n", p.IfUpFile(), err) 47 | } 48 | 49 | ifupTpl.Execute(wr, network) 50 | 51 | wr.Close() 52 | 53 | wr, err = os.OpenFile(p.IfDownFile(), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) 54 | if err != nil { 55 | return Network{}, fmt.Errorf("error creating %s file - %s\n", p.IfUpFile(), err) 56 | } 57 | 58 | ifdownTpl.Execute(wr, nil) 59 | 60 | wr.Close() 61 | 62 | return network, nil 63 | } 64 | 65 | func (n Network) generateMAC() string { 66 | buf := make([]byte, 3) 67 | 68 | _, err := rand.Read(buf) 69 | if err != nil { 70 | return "" 71 | } 72 | 73 | return fmt.Sprintf("52:54:00:%02x:%02x:%02x", buf[0], buf[1], buf[2]) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/network/network_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGenerateMAC(t *testing.T) { 9 | for i := 0; i < 10; i++ { 10 | fmt.Println((Network{}).generateMAC()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pkg/project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/deferpanic/dpcli/api" 14 | "github.com/deferpanic/virgo/pkg/depcheck" 15 | "github.com/deferpanic/virgo/pkg/network" 16 | "github.com/deferpanic/virgo/pkg/registry" 17 | "github.com/deferpanic/virgo/pkg/runner" 18 | "github.com/deferpanic/virgo/pkg/tools" 19 | ) 20 | 21 | type Project struct { 22 | registry.Project 23 | manifest api.Manifest 24 | Process runner.Runner 25 | Network network.Network 26 | num int 27 | } 28 | 29 | func New(pr registry.Project, n network.Network, r runner.Runner, projectNum int) (*Project, error) { 30 | p := &Project{ 31 | Network: n, 32 | Project: pr, // initialize project registry 33 | Process: r, 34 | num: projectNum, 35 | } 36 | 37 | b, err := ioutil.ReadFile(p.ManifestFile()) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if err := json.Unmarshal(b, &p.manifest); err != nil { 43 | return nil, fmt.Errorf("unable to load manifest file - %s", err) 44 | } 45 | 46 | return p, nil 47 | } 48 | 49 | func (p *Project) Run(headless bool) error { 50 | var ( 51 | env string 52 | bootLine []string 53 | kflag string 54 | nographic string 55 | ) 56 | 57 | if len(p.manifest.Processes) == 0 { 58 | return fmt.Errorf("no processes found in manifest file, unable to proceed") 59 | } 60 | 61 | blocks, drives := p.createQemuBlocks() 62 | 63 | if p.manifest.Processes[0].Env != "" { 64 | env = p.formatEnv(p.manifest.Processes[0].Env) 65 | } 66 | 67 | ip := p.Network.Ip 68 | gw := p.Network.Gw 69 | 70 | appendline := `{"net" : {"if":"vioif0", "type":"inet", "method":"static", "addr":"` + ip + `", "mask":"24", "gw":"` + gw + `"}, ` + env + blocks + ` "cmdline": "` + p.manifest.Processes[0].Cmdline + `"}` 71 | 72 | if p.manifest.Processes[0].Multiboot { 73 | bootLine = []string{"-kernel", p.KernelFile(), "-append", appendline} 74 | } else { 75 | bootLine = []string{"-hda", p.KernelFile()} 76 | } 77 | 78 | switch runtime.GOOS { 79 | case "linux": 80 | if p.kvmEnabled() { 81 | kflag = "-enable-kvm" 82 | } 83 | case "darwin": 84 | dep := depcheck.New(p.Process) 85 | 86 | if err := dep.RunAll(); err != nil { 87 | return err 88 | } 89 | 90 | if dep.HasHAX() { 91 | kflag = "-accel hax" 92 | } 93 | default: 94 | kflag = "-no-kvm" 95 | } 96 | 97 | if headless { 98 | nographic = "-nographic" 99 | } 100 | 101 | mac := p.Network.Mac 102 | num := strconv.Itoa(p.num) 103 | 104 | cmd := "qemu-system-x86_64" 105 | args := []string{ 106 | kflag, 107 | nographic, 108 | "-serial", "file:" + p.LogsDir() + "/blah.log", 109 | "-vga", "none", 110 | "-m", strconv.Itoa(p.manifest.Processes[0].Memory), 111 | "-netdev", "tap,id=vmnet" + num + ",ifname=tap" + num + ",script=" + p.Root() + "/ifup.sh,downscript=" + p.Root() + "/ifdown.sh", 112 | "-device", "virtio-net-pci,netdev=vmnet" + num + ",mac=" + mac, 113 | } 114 | args = append(args, drives...) 115 | args = append(args, bootLine...) 116 | 117 | p.Process.SetDetached(true) 118 | 119 | if err := p.Process.Exec(cmd, args...); err != nil { 120 | return fmt.Errorf("error running '%s %s' - %s", cmd, tools.Join(args, " "), err) 121 | } 122 | 123 | // log.Printf("open up http://%s:3000", ip) 124 | 125 | return nil 126 | } 127 | 128 | func (p *Project) formatEnv(env string) (result string) { 129 | parts := strings.Split(env, " ") 130 | 131 | for i, _ := range parts { 132 | result += `"env": "` + parts[i] + `",` 133 | } 134 | 135 | return result 136 | } 137 | 138 | // locked down to one process for now 139 | // 140 | func (p *Project) createQemuBlocks() (string, []string) { 141 | blocks := "" 142 | drives := []string{} 143 | 144 | if len(p.manifest.Processes) == 0 { 145 | return blocks, drives 146 | } 147 | 148 | for i, volume := range p.manifest.Processes[0].Volumes { 149 | blocks += `"blk" : {"source":"dev", "path":"/dev/ld` + 150 | strconv.Itoa(i) + `a", "fstype":"blk", "mountpoint":"` + 151 | volume.Mount + `"}, ` 152 | drives = append(drives, []string{"-drive", "if=virtio,file=" + p.VolumesDir() + "/vol" + strconv.Itoa(volume.Id) + ",format=raw"}...) 153 | } 154 | 155 | return blocks, drives 156 | } 157 | 158 | func (p *Project) kvmEnabled() bool { 159 | out, err := p.Process.Shell("egrep '(vmx|svm)' /proc/cpuinfo") 160 | if err != nil { 161 | log.Printf("Error retrieving KVM status - %s\n", err) 162 | return false 163 | } 164 | 165 | out = bytes.TrimSpace(out) 166 | 167 | if len(out) == 0 { 168 | return false 169 | } 170 | 171 | return true 172 | } 173 | -------------------------------------------------------------------------------- /pkg/project/projects.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/deferpanic/virgo/pkg/network" 12 | "github.com/deferpanic/virgo/pkg/registry" 13 | "github.com/deferpanic/virgo/pkg/runner" 14 | "github.com/deferpanic/virgo/pkg/tools" 15 | ) 16 | 17 | type Runtime struct { 18 | ProjectName string 19 | Process []*runner.ExecRunner 20 | Network []network.Network 21 | } 22 | 23 | type Projects []*Runtime 24 | 25 | func LoadProjects(r *registry.Registry) (Projects, error) { 26 | result := make(Projects, 0) 27 | 28 | b, err := ioutil.ReadFile(r.RuntimeFile()) 29 | if err != nil && os.IsNotExist(err) { 30 | return result, nil 31 | } 32 | if err != nil { 33 | return nil, fmt.Errorf("error reading %s - %s", r.RuntimeFile(), err) 34 | } 35 | 36 | if err := json.Unmarshal(b, &result); err != nil { 37 | return nil, fmt.Errorf("error unmarshalling %s - %s", r.RuntimeFile(), err) 38 | } 39 | 40 | return result, nil 41 | } 42 | 43 | func (ps Projects) GetProjectByName(name string) *Runtime { 44 | for _, p := range ps { 45 | if p.ProjectName == name { 46 | return p 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (ps Projects) Add(p *Project, r *registry.Registry) error { 54 | if _, ok := p.Process.(runner.DryRunner); ok { 55 | return nil 56 | } 57 | 58 | for i, _ := range ps { 59 | if ps[i].ProjectName == p.Name() { 60 | ps[i].Process = append(ps[i].Process, p.Process.(*runner.ExecRunner)) 61 | ps[i].Network = append(ps[i].Network, p.Network) 62 | return ps.save(r) 63 | } 64 | } 65 | 66 | rt := &Runtime{ 67 | ProjectName: p.Name(), 68 | Process: []*runner.ExecRunner{p.Process.(*runner.ExecRunner)}, 69 | Network: []network.Network{p.Network}, 70 | } 71 | 72 | ps = append(ps, rt) 73 | 74 | return ps.save(r) 75 | } 76 | 77 | func (ps Projects) Delete(rt *Runtime, r *registry.Registry) error { 78 | for i, _ := range ps { 79 | if ps[i].ProjectName == rt.ProjectName { 80 | ps = append(ps[:i], ps[i+1:]...) 81 | return ps.save(r) 82 | } 83 | } 84 | 85 | return fmt.Errorf("project '%s' not found in runtime", rt.ProjectName) 86 | } 87 | 88 | func (ps Projects) save(r *registry.Registry) error { 89 | wr, err := os.OpenFile(r.RuntimeFile(), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 90 | if err != nil { 91 | return fmt.Errorf("error opening file '%s' - %s", r.RuntimeFile(), err) 92 | } 93 | defer wr.Close() 94 | 95 | b, err := json.Marshal(ps) 96 | if err != nil { 97 | return fmt.Errorf("error marshalling Projects - %s", err) 98 | } 99 | 100 | if _, err := wr.Write(b); err != nil { 101 | return fmt.Errorf("error writing file '%s' - %s", r.RuntimeFile(), err) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (ps Projects) Running() Projects { 108 | result := make(Projects, 0) 109 | 110 | for _, p := range ps { 111 | // for now it doesn't matter how many instances are running 112 | if len(p.Process) > 0 && p.Process[0].IsAlive() { 113 | result = append(result, p) 114 | } 115 | } 116 | 117 | return result 118 | } 119 | 120 | func (ps Projects) GetNextNetowrk() (string, string) { 121 | highIP := net.IP{10, 1, 2, 4}.To4() 122 | highGw := net.IP{10, 1, 2, 1}.To4() 123 | 124 | if len(ps.Running()) == 0 { 125 | return highIP.To4().String(), highGw.To4().String() 126 | } 127 | 128 | if len(ps.Running()) > 0 { 129 | for _, p := range ps { 130 | ip := net.ParseIP(p.Network[len(p.Network)-1].Ip).To4() 131 | if ip[2] > highIP[2] { 132 | highIP = ip 133 | } 134 | } 135 | 136 | highIP[2]++ 137 | 138 | if highIP[2] == 255 { 139 | return "", "" 140 | } 141 | } 142 | 143 | highGw[2] = highIP[2] 144 | 145 | return highIP.To4().String(), highGw.To4().String() 146 | } 147 | 148 | func (ps Projects) NextNum() int { 149 | var n int = 0 150 | 151 | for i, _ := range ps { 152 | n += len(ps[i].Process) 153 | } 154 | 155 | return n + 1 156 | } 157 | 158 | func (ps Projects) String() string { 159 | var result string 160 | 161 | if len(ps) == 0 { 162 | return "" 163 | } 164 | 165 | result += "Projectname\tGw\tIP\tMAC\tPids\n" 166 | 167 | for _, p := range ps { 168 | pids := []string{} 169 | 170 | for _, instance := range p.Process { 171 | pids = append(pids, strconv.Itoa(instance.Pid)) 172 | } 173 | 174 | result += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", p.ProjectName, p.Network[0].Gw, p.Network[0].Ip, p.Network[0].Mac, tools.Join(pids, ", ")) 175 | 176 | for i := 1; i < len(p.Network); i++ { 177 | result += fmt.Sprintf("\t%s\t%s\t%s\n", p.Network[i].Gw, p.Network[i].Ip, p.Network[i].Mac) 178 | } 179 | 180 | } 181 | 182 | return result 183 | } 184 | -------------------------------------------------------------------------------- /pkg/project/projects_test.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | 8 | "github.com/deferpanic/virgo/pkg/registry" 9 | ) 10 | 11 | func writeSampleData(file string, b []byte) error { 12 | wr, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 13 | if err != nil { 14 | return err 15 | } 16 | defer wr.Close() 17 | 18 | if _, err := wr.Write(b); err != nil { 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func TestProjects(t *testing.T) { 26 | type sampledate struct { 27 | name string 28 | manifest string 29 | pidfile string 30 | } 31 | 32 | runtimeSample := `[ 33 | { 34 | "ProjectName": "project1", 35 | "Process": [{ 36 | "Pid": 0 37 | }] 38 | }, 39 | { 40 | "ProjectName": "project2", 41 | "Process": [{ 42 | "Pid": 123 43 | }] 44 | }, 45 | { 46 | "ProjectName": "project3", 47 | "Process": [{ 48 | "Pid": 234 49 | }] 50 | } 51 | ]` 52 | 53 | sd := []sampledate{ 54 | { 55 | name: "project1", 56 | manifest: `{"Processes":[{"Memory":64,"Kernel":"project1","Multiboot":true,"Hash":"00000000000000000000000000000000","Cmdline":" ","Env":"","Volumes":[{"Id":7887,"File":"stubetc.iso","Mount":"/etc"}]}]}`, 57 | }, 58 | { 59 | name: "project2", 60 | manifest: `{"Processes":[{"Memory":64,"Kernel":"project2","Multiboot":true,"Hash":"00000000000000000000000000000000","Cmdline":" ","Env":"","Volumes":[{"Id":7888,"File":"stubetc.iso","Mount":"/etc"}]}]}`, 61 | }, 62 | { 63 | name: "project3", 64 | manifest: `{"Processes":[{"Memory":64,"Kernel":"project3","Multiboot":true,"Hash":"00000000000000000000000000000000","Cmdline":" ","Env":"","Volumes":[{"Id":7889,"File":"stubetc.iso","Mount":"/etc"}]}]}`, 65 | }, 66 | } 67 | 68 | r, err := registry.New("/tmp/.virgo") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | if err := writeSampleData(r.RuntimeFile(), []byte(runtimeSample)); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | for _, sample := range sd { 78 | if _, err := r.AddProject(sample.name); err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | if err := writeSampleData(r.Project(sample.name).ManifestFile(), []byte(sample.manifest)); err != nil { 83 | t.Fatal(err) 84 | } 85 | } 86 | 87 | projects, err := LoadProjects(r) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if n := len(projects); n != 3 { 93 | t.Fatalf("Expected legth is 3, obtained %d\n", len(projects)) 94 | } 95 | 96 | // We can't test here actual state, because of fake input data 97 | // @TODO change it to real and then it will be possible 98 | // 99 | // if running := len(projects.Running()); running != 2 { 100 | // t.Fatalf("Expected running is 2, obtained %d\n", running) 101 | // } 102 | 103 | // Fake test to fake data 104 | running := 0 105 | for _, p := range projects { 106 | for _, proc := range p.Process { 107 | if proc.Pid != 0 { 108 | running += 1 109 | } 110 | } 111 | } 112 | 113 | if running != 2 { 114 | t.Fatalf("Expected running is 2, obtained %d\n", running) 115 | } 116 | } 117 | 118 | func TestNextNetPair(t *testing.T) { 119 | highIP := net.IP{10, 1, 2, 4}.To4() 120 | highGw := net.IP{10, 1, 2, 1}.To4() 121 | 122 | for { 123 | ip := net.ParseIP("10.1.2.4").To4() 124 | if ip[2] > highIP[2] { 125 | highIP = ip 126 | } 127 | 128 | highIP[2]++ 129 | highGw[2]++ 130 | 131 | if highIP[2] == 255 { 132 | break 133 | } 134 | } 135 | 136 | if highIP.To4().String() != net.ParseIP("10.1.255.4").To4().String() { 137 | t.Fatalf("Expected IP: 10.1.255.4, Obtained: %s\n", highIP.To4().String()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/project/pull.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/deferpanic/dpcli/api" 11 | "github.com/deferpanic/virgo/pkg/registry" 12 | ) 13 | 14 | func Pull(pr registry.Project) error { 15 | var ( 16 | err error 17 | manifest api.Manifest 18 | ) 19 | 20 | if manifest, err = api.LoadManifest(pr.Name()); err != nil { 21 | return err 22 | } 23 | 24 | ap := &api.Projects{} 25 | if pr.IsCommunity() { 26 | parts := strings.Split(pr.Name(), "/") 27 | err = ap.DownloadCommunity(parts[1], pr.UserName(), pr.KernelFile()) 28 | } else { 29 | err = ap.Download(pr.Name(), pr.KernelFile()) 30 | } 31 | if err != nil { 32 | return err 33 | } 34 | 35 | v := &api.Volumes{} 36 | 37 | for i := 0; i < len(manifest.Processes); i++ { 38 | proc := manifest.Processes[i] 39 | for _, volume := range proc.Volumes { 40 | dst := filepath.Join(pr.VolumesDir(), "vol"+strconv.Itoa(volume.Id)) 41 | 42 | if err = v.Download(volume.Id, dst); err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | 48 | b, err := json.Marshal(manifest) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | wr, err := os.OpenFile(pr.ManifestFile(), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if _, err := wr.Write(b); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | cfgDefaultRoot = ".virgo" 13 | cfgProjectsDir = "projects" 14 | cfgKernelDir = "kernel" 15 | cfgLogsDir = "logs" 16 | cfgPidsDir = "pids" 17 | cfgVolumesDir = "volumes" 18 | cfgManifestFile = "manifest" 19 | cfgRuntimeFile = "runtime.json" 20 | cfgIfUpFile = "ifup.sh" 21 | cfgIfDownFile = "ifdown.sh" 22 | ) 23 | 24 | type Project struct { 25 | name string 26 | username string 27 | root string 28 | } 29 | 30 | type Registry struct { 31 | root string 32 | projects []Project 33 | } 34 | 35 | // "v ...string" is optional argument, for non-default registry root 36 | func New(v ...string) (r *Registry, err error) { 37 | r = &Registry{ 38 | root: filepath.Join(os.Getenv("HOME"), cfgDefaultRoot), 39 | } 40 | 41 | if len(v) == 1 { 42 | r.root = v[0] 43 | } 44 | 45 | if err = r.initialize(); err != nil { 46 | return 47 | } 48 | 49 | return 50 | } 51 | 52 | func (r *Registry) AddProject(name string) (Project, error) { 53 | p := Project{name: name, root: r.root} 54 | 55 | if name == "" { 56 | return Project{}, fmt.Errorf("empty project name, unable to proceed") 57 | } 58 | 59 | if strings.Contains(name, "/") { 60 | if parts := strings.Split(name, "/"); len(parts) != 2 { 61 | return Project{}, fmt.Errorf("wrong format for community project, should be project/username") 62 | } else { 63 | if parts[1] == "" { 64 | return Project{}, fmt.Errorf("username can't be empty for community projects") 65 | } 66 | 67 | p.username = parts[0] 68 | } 69 | } 70 | 71 | p.name = name 72 | r.projects = append(r.projects, p) 73 | 74 | projectroot := filepath.Join(r.Projects(), name) 75 | 76 | if _, err := os.Stat(projectroot); err == nil { 77 | return p, nil 78 | } 79 | 80 | for _, dir := range r.Project(name).Structure() { 81 | if err := os.MkdirAll(dir, 0755); err != nil { 82 | return Project{}, fmt.Errorf("error creating registry - %s", err) 83 | } 84 | } 85 | 86 | return p, nil 87 | } 88 | 89 | func (r *Registry) Project(name string) Project { 90 | for _, project := range r.projects { 91 | if project.name == name { 92 | return project 93 | } 94 | } 95 | 96 | return Project{} 97 | } 98 | 99 | func (r *Registry) ProjectList() []Project { 100 | return r.projects 101 | } 102 | 103 | func (r *Registry) initialize() error { 104 | if _, err := os.Stat(r.Root()); err != nil { 105 | if os.IsNotExist(err) { 106 | for _, dir := range r.Structure() { 107 | if err := os.MkdirAll(dir, 0755); err != nil { 108 | return fmt.Errorf("error creating registry - %s", err) 109 | } 110 | } 111 | return nil 112 | } else if os.IsExist(err) { 113 | } else { 114 | return fmt.Errorf("error initializing registry - %s", err) 115 | } 116 | } 117 | 118 | var loadProjects func(root string) error 119 | 120 | loadProjects = func(root string) error { 121 | list, err := ioutil.ReadDir(root) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | for _, info := range list { 127 | if !info.IsDir() { 128 | continue 129 | } 130 | 131 | manifest := filepath.Join(root, info.Name(), info.Name()+"."+cfgManifestFile) 132 | 133 | if _, err := os.Stat(manifest); err == nil { 134 | projectName := info.Name() 135 | 136 | if root != r.Projects() { 137 | projectName = filepath.Join(filepath.Base(root), info.Name()) 138 | } 139 | 140 | if _, err := r.AddProject(projectName); err != nil { 141 | return err 142 | } 143 | } else if os.IsNotExist(err) { 144 | loadProjects(filepath.Join(root, info.Name())) 145 | } else { 146 | fmt.Println(err) 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | 153 | return loadProjects(r.Projects()) 154 | } 155 | 156 | func (r Registry) purge() error { 157 | return os.RemoveAll(r.Root()) 158 | } 159 | 160 | func (r Registry) PurgeProject(name string) error { 161 | return os.RemoveAll(r.Project(name).Root()) 162 | } 163 | 164 | func (r Registry) Root() string { 165 | return r.root 166 | } 167 | 168 | func (r Registry) Projects() string { 169 | return filepath.Join(r.root, cfgProjectsDir) 170 | } 171 | 172 | func (r Registry) RuntimeFile() string { 173 | return filepath.Join(r.root, cfgRuntimeFile) 174 | } 175 | 176 | func (r Registry) Structure() []string { 177 | return []string{ 178 | r.Root(), 179 | r.Projects(), 180 | } 181 | } 182 | 183 | // Returns project root, e.g.: ~/.virgo/projects/hello 184 | // For community projects root is nested in username/projects directory. 185 | func (p Project) Root() string { 186 | return filepath.Join(p.root, cfgProjectsDir, p.name) 187 | } 188 | 189 | func (p Project) Name() string { 190 | return p.name 191 | } 192 | 193 | func (p Project) LogsDir() string { 194 | return filepath.Join(p.Root(), cfgLogsDir) 195 | } 196 | 197 | func (p Project) KernelDir() string { 198 | return filepath.Join(p.Root(), cfgKernelDir) 199 | } 200 | 201 | func (p Project) KernelFile() string { 202 | file := filepath.Join(p.Root(), cfgKernelDir, p.Name()) 203 | 204 | if p.IsCommunity() { 205 | name := strings.Replace(p.Name(), "/", "_", -1) 206 | file = filepath.Join(p.Root(), cfgKernelDir, name) 207 | } 208 | 209 | return file 210 | } 211 | 212 | func (p Project) VolumesDir() string { 213 | return filepath.Join(p.Root(), cfgVolumesDir) 214 | } 215 | 216 | func (p Project) ManifestFile() string { 217 | file := filepath.Join(p.Root(), p.Name()+"."+cfgManifestFile) 218 | 219 | if p.IsCommunity() { 220 | parts := strings.Split(p.Name(), "/") 221 | file = filepath.Join(p.Root(), parts[1]+"."+cfgManifestFile) 222 | } 223 | 224 | return file 225 | } 226 | 227 | func (p Project) IfUpFile() string { 228 | return filepath.Join(p.Root(), cfgIfUpFile) 229 | } 230 | 231 | func (p Project) IfDownFile() string { 232 | return filepath.Join(p.Root(), cfgIfDownFile) 233 | } 234 | 235 | func (p Project) IsCommunity() bool { 236 | return p.username != "" 237 | } 238 | 239 | func (p Project) UserName() string { 240 | return p.username 241 | } 242 | 243 | func (p Project) Structure() []string { 244 | return []string{ 245 | p.Root(), 246 | p.LogsDir(), 247 | p.KernelDir(), 248 | p.VolumesDir(), 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /pkg/registry/registry_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "testing" 7 | 8 | "github.com/deferpanic/virgo/pkg/tools" 9 | ) 10 | 11 | func expectedRoot() []string { 12 | return []string{ 13 | "/tmp/.virgo", 14 | "/tmp/.virgo/projects", 15 | } 16 | } 17 | 18 | func expectedProject(name string) []string { 19 | return []string{ 20 | "/tmp/.virgo/projects/" + name, 21 | "/tmp/.virgo/projects/" + name + "/logs", 22 | "/tmp/.virgo/projects/" + name + "/kernel", 23 | "/tmp/.virgo/projects/" + name + "/volumes", 24 | 25 | // we do not create this files, so no test for it 26 | // "/tmp/.virgo/projects/" + name + "/manifest", 27 | // "/tmp/.virgo/projects/" + name + "/pid.json", 28 | } 29 | } 30 | 31 | func TestRegistryRoot(t *testing.T) { 32 | r, err := New("/tmp/.virgo") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer r.purge() 37 | 38 | obtained := r.Structure() 39 | 40 | for _, path := range expectedRoot() { 41 | if !(tools.StringSlice)(obtained).Contains(path) { 42 | t.Errorf("obtained structure doesn't contain '%s'\n", path) 43 | } 44 | } 45 | 46 | for _, path := range obtained { 47 | if !(tools.StringSlice)(expectedRoot()).Contains(path) { 48 | t.Errorf("test structure doesn't contain '%s'\n", path) 49 | } 50 | } 51 | 52 | if t.Failed() { 53 | t.Error(obtained) 54 | t.FailNow() 55 | } 56 | } 57 | 58 | func TestProjectRoot(t *testing.T) { 59 | projectName := fmt.Sprintf("testing-%d", rand.Intn(65535)) 60 | 61 | r, err := New("/tmp/.virgo") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer r.purge() 66 | 67 | if _, err := r.AddProject(projectName); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | obtained := r.Project(projectName).Structure() 72 | 73 | for _, path := range expectedProject(projectName) { 74 | if !(tools.StringSlice)(obtained).Contains(path) { 75 | t.Errorf("obtained structure doesn't contain '%s'\n", path) 76 | } 77 | } 78 | 79 | for _, path := range obtained { 80 | if !(tools.StringSlice)(expectedProject(projectName)).Contains(path) { 81 | t.Errorf("test structure doesn't contain '%s'\n", path) 82 | } 83 | } 84 | 85 | if t.Failed() { 86 | t.Errorf("%v\n", obtained) 87 | t.Errorf("%v\n", expectedProject(projectName)) 88 | t.FailNow() 89 | } 90 | } 91 | 92 | func TestRegistryCommunity(t *testing.T) { 93 | r, err := New("/tmp/.virgo") 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | defer r.purge() 98 | 99 | if _, err = r.AddProject("project/asdf"); err != nil { 100 | t.Error(err) 101 | } 102 | 103 | if _, err = r.AddProject("project2/"); err == nil { 104 | t.Error("Expecting error for empty username") 105 | } 106 | 107 | if _, err = r.AddProject("project/asdf/adsf"); err == nil { 108 | t.Error("Expecting error for wrong format") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | 11 | "github.com/deferpanic/virgo/pkg/tools" 12 | ) 13 | 14 | type Runner interface { 15 | Exec(name string, args ...string) error 16 | Run(name string, args ...string) ([]byte, error) 17 | Shell(args string) ([]byte, error) 18 | SetDetached(v bool) 19 | IsAlive() bool 20 | } 21 | 22 | type ExecRunner struct { 23 | stdout *os.File 24 | stderr *os.File 25 | proc *exec.Cmd 26 | Detached bool 27 | Pid int 28 | } 29 | 30 | func NewExecRunner(stdout, stderr *os.File, detached bool) *ExecRunner { 31 | return &ExecRunner{ 32 | stdout: stdout, 33 | stderr: stderr, 34 | Detached: detached, 35 | } 36 | } 37 | 38 | func (r *ExecRunner) Exec(name string, args ...string) error { 39 | var err error 40 | 41 | cleaned := []string{} 42 | 43 | for i := 0; i < len(args); i++ { 44 | if args[i] == " " || args[i] == "" { 45 | continue 46 | } 47 | 48 | cleaned = append(cleaned, args[i]) 49 | } 50 | 51 | r.proc = exec.Command(name, cleaned...) 52 | 53 | if r.Detached { 54 | r.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 55 | } 56 | 57 | r.proc.Stdout = r.stdout 58 | r.proc.Stderr = r.stderr 59 | 60 | if err = r.proc.Start(); err != nil { 61 | return err 62 | } 63 | 64 | done := make(chan error, 1) 65 | 66 | go func() { 67 | done <- r.proc.Wait() 68 | 69 | for { 70 | select { 71 | case err := <-done: 72 | if err != nil { 73 | return 74 | } 75 | } 76 | } 77 | }() 78 | 79 | r.Pid = r.proc.Process.Pid 80 | 81 | return err 82 | } 83 | 84 | func (r *ExecRunner) Run(name string, args ...string) ([]byte, error) { 85 | return exec.Command(tools.Join(args, " ")).CombinedOutput() 86 | } 87 | 88 | func (r *ExecRunner) Shell(args string) ([]byte, error) { 89 | return exec.Command("/bin/sh", "-c", args).CombinedOutput() 90 | } 91 | 92 | func (r *ExecRunner) SetDetached(v bool) { 93 | r.Detached = v 94 | } 95 | 96 | func (r *ExecRunner) IsAlive() bool { 97 | // this is wrong, but temporary needed 98 | if r.Pid != 0 { 99 | return true 100 | } 101 | 102 | if r.proc == nil || r.proc.Process == nil || r.proc.Process.Pid == 0 { 103 | return false 104 | } 105 | 106 | if err := r.proc.Process.Signal(syscall.Signal(0)); err == nil { 107 | return true 108 | } 109 | 110 | return false 111 | } 112 | 113 | func (r *ExecRunner) Stop() error { 114 | var ( 115 | pid int 116 | ) 117 | 118 | if r.proc == nil || r.proc.Process == nil || r.proc.Process.Pid == 0 { 119 | return fmt.Errorf("no process to stop") 120 | } else { 121 | pid = r.proc.Process.Pid 122 | } 123 | 124 | if r.Detached { 125 | pgid, err := syscall.Getpgid(pid) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | if err = syscall.Kill(pgid, syscall.SIGTERM); err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | 137 | r.proc.Process.Kill() 138 | r.proc.Wait() 139 | 140 | return nil 141 | } 142 | 143 | func (r *ExecRunner) UnmarshalJSON(b []byte) error { 144 | type tmp ExecRunner 145 | 146 | t := &tmp{} 147 | 148 | if err := json.Unmarshal(b, t); err != nil { 149 | return err 150 | } 151 | 152 | p, err := os.FindProcess(t.Pid) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | r.stdout = os.Stdout 158 | r.stderr = os.Stderr 159 | r.proc = &exec.Cmd{Process: p} 160 | r.Detached = t.Detached 161 | r.Pid = t.Pid 162 | 163 | return nil 164 | } 165 | 166 | type DryRunner struct { 167 | output io.Writer 168 | } 169 | 170 | func NewDryRunner(o io.Writer) DryRunner { 171 | return DryRunner{ 172 | output: o, 173 | } 174 | } 175 | 176 | func (r DryRunner) Exec(name string, args ...string) error { 177 | _, err := fmt.Fprintf(r.output, "%s %s", name, tools.Join(args, " ")) 178 | 179 | return err 180 | } 181 | 182 | func (r DryRunner) Run(name string, args ...string) ([]byte, error) { 183 | _, err := fmt.Fprintf(r.output, "%s %s\n", name, tools.Join(args, " ")) 184 | 185 | return nil, err 186 | } 187 | 188 | func (r DryRunner) SetDetached(v bool) {} 189 | func (r DryRunner) IsAlive() bool { return false } 190 | func (r DryRunner) Shell(args string) ([]byte, error) { 191 | _, err := fmt.Println("/bin/sh", "-c", args) 192 | 193 | return nil, err 194 | } 195 | -------------------------------------------------------------------------------- /pkg/runner/runner_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestDryRun(t *testing.T) { 11 | name := "ping" 12 | args := []string{"-c", "10", "127.0.0.1"} 13 | expected := []byte("ping -c 10 127.0.0.1") 14 | 15 | output := bytes.Buffer{} 16 | w := bufio.NewWriter(&output) 17 | 18 | if err := NewDryRunner(w).Exec(name, args...); err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | w.Flush() 23 | 24 | if !bytes.Equal(output.Bytes(), expected) { 25 | t.Fatalf("Expected output is '%s', obtained: '%s'\n", string(expected), string(output.Bytes())) 26 | } 27 | 28 | if err := NewDryRunner(os.Stdout).Exec(name, args...); err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestProcess(t *testing.T) { 34 | name := "ping" 35 | args := []string{"-c", "23", "127.0.0.1"} 36 | 37 | // this test only covers process in same group 38 | // for detached processes it will be failed on last isAlive check 39 | p := NewExecRunner(os.Stdout, os.Stderr, false) 40 | 41 | if err := p.Exec(name, args...); err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if !p.IsAlive() { 46 | t.Fatal("No such process found, expecting it's alive") 47 | } 48 | 49 | if err := p.Stop(); err != nil { 50 | t.Fatal(err) 51 | } 52 | } 53 | 54 | func TestBashReturnValue(t *testing.T) { 55 | r := NewExecRunner(os.Stdout, os.Stderr, false) 56 | 57 | _, err := r.Shell("ps | grep -c 1") 58 | if err != nil { 59 | t.Fatalf("Unexpected error: return code should be 0, obtained - %v\n", err) 60 | } 61 | 62 | _, err = r.Shell("ls | grep -c /tmp/555/nosuchfile") 63 | if err == nil { 64 | t.Fatalf("Unexpected error: return code should be 1, obtained - %v\n", err) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /pkg/tools/logo.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | var Logo = `                                         4 |                                          5 |            ..'',,,,,,,,,''..             6 |         .',,...............,,'.          7 |       ',,. ..',;;;;;;;;;    ..,,'        8 |     .,,. .,;;;;;;;;;;;;;   .,. .,,.      9 |    ',' .,;;;;;;,'....',;   .;;,. .,,     10 |   ',' .;;;;;;'             .;;;;' .,,    11 |  .,, .;;;;;;.   .',,,'.    .;;;;;. ,,.   12 |  ',. ,;;;;;'   ',',',,,'     .,;;, .,,   13 |  ,,. ,;;,......;,';;;;';....,,,;;; .,,   14 |  ',. ';;;,.    .,,,',,;.   ,;;;;;, .,'   15 |  .,,..,;;;;.     .....    .;;;;;;. ,,.   16 |   .,, .,;;;.   .       ..,;;;;;,. ,,.    17 |    .,,. .,;.   ,;;;,,;;;;;;;;;' .,,.     18 |      ','. ..   ,;;;;;;;;;;;,. .',,.      19 |       .',,..   .,,;;;,,'.....,,'.        20 |          ..,,,'.........'',,'.           21 |              ....'''''....               22 |       23 |       24 |                         ` 25 | -------------------------------------------------------------------------------- /pkg/tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | "github.com/deferpanic/dpcli/api" 11 | ) 12 | 13 | // custom strings concatenator to avoid separator artefacts of empty params 14 | func Join(a []string, sep string) string { 15 | result := make([]byte, 0) 16 | 17 | for i, _ := range a { 18 | if len(a[i]) == 0 { 19 | continue 20 | } 21 | 22 | if len(result) > 0 && a[i] != "" { 23 | result = append(result, []byte(sep)...) 24 | } 25 | 26 | result = append(result, []byte(a[i])...) 27 | } 28 | 29 | return string(result) 30 | } 31 | 32 | type Slice interface { 33 | Contains(string) bool 34 | } 35 | 36 | type StringSlice []string 37 | 38 | func (ss StringSlice) Contains(s string) bool { 39 | for i, _ := range ss { 40 | if ss[i] == s { 41 | return true 42 | } 43 | } 44 | 45 | return false 46 | } 47 | 48 | func SetToken() error { 49 | f := os.Getenv("HOME") + "/.dprc" 50 | 51 | dat, err := ioutil.ReadFile(f) 52 | if err != nil { 53 | return fmt.Errorf("error reading file '%s' - %s", f, err) 54 | } 55 | 56 | dtoken := string(dat) 57 | 58 | if dtoken == "" { 59 | return fmt.Errorf("error reading token - no token found") 60 | } 61 | 62 | dtoken = strings.TrimSpace(dtoken) 63 | api.Cli = api.NewCliImplementation(dtoken) 64 | 65 | return nil 66 | } 67 | 68 | func ShowFiles(dir string) error { 69 | fd, err := ioutil.ReadDir(dir) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | w := tabwriter.NewWriter(os.Stdout, 1, 8, 2, '\t', 0) 75 | 76 | for _, f := range fd { 77 | if f.IsDir() { 78 | continue 79 | } 80 | 81 | fmt.Fprintf(w, "%s\t%d\t%s\n", f.Name(), f.Size(), f.ModTime().String()) 82 | fmt.Fprintln(w, "----") 83 | 84 | path := dir + string('/') + f.Name() 85 | bytes, err := ioutil.ReadFile(path) 86 | if err != nil { 87 | return fmt.Errorf("error reading log file '%s' - '%s'", path, err) 88 | } 89 | fmt.Fprintf(w, "%s\n\n", bytes) 90 | } 91 | 92 | err = w.Flush() 93 | if err != nil { 94 | return fmt.Errorf("error flushing log output '%s'", err) 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: golang 2 | 3 | build: 4 | steps: 5 | - setup-go-workspace 6 | 7 | - script: 8 | name: go get 9 | code: | 10 | go get ./... 11 | 12 | - script: 13 | name: go build 14 | code: | 15 | go build ./... 16 | 17 | - script: 18 | name: go test 19 | code: | 20 | go test ./... 21 | --------------------------------------------------------------------------------