diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8000dd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9bb77b7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015 David Calavera + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a086259 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Docker volume plugin for GlusterFS + +Very very experimental + +## LICENSE + +MIT diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..c88f172 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,88 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +user_config = """ +adduser --disabled-password david +adduser david sudo +chown -R david /home/david/go + +mkdir -p /home/david/.ssh +chown -R david /home/david +cat << END > /home/david/.ssh/authorized_keys +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFoY7zn6ZP4EgovBHnVsqPeJ16LuRw4u0Yv8ScHGCMIRMTUM1vW+hO8VIH7DjgabhvzV/OJ4/BEFAJ8NYVouTsW89+vPHqJtWpMUqUN1iCGahYKwXgTXNuHCv+NUMc2rrHP+hizDc/s64djxdGT6iMNKHg9uLv7HLGQFjVSXmCK9Mrdg+d/H3Yhrsoqavdn61Y/H7CxMCvaGsnFIDPsI/BkG4p28GsNPyFpIZoPXdbBXwyaU6EGTPgQgpizbZ1HkMTKNYJeLQLP05Uwa/5KHLZAp74UVYfaSXTqsZrDtGZ8Q4pbKsQ11jrOj99vIDSs9el/9FT0pYaqEMPKbur/5wD david.calavera@gmail.com +END + +cat << END > /etc/sudoers.d/david +david ALL=(ALL) NOPASSWD:ALL +END +""" + +hosts_config = """cat << END >> /etc/hosts +172.21.12.11 gfs-server-1 +172.21.12.12 gfs-server-2 +172.21.12.13 gfs-server-3 + +172.21.12.10 gfs-client-1 +172.21.12.20 gfs-client-2 +END +""" + +server_shell = """ +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq python-software-properties +DEBIAN_FRONTEND=noninteractive add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.5 +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq glusterfs-server + +#{user_config} + +#{hosts_config} +""" + +client_shell = %Q{ +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq python-software-properties +DEBIAN_FRONTEND=noninteractive add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.5 +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq glusterfs-client + +#{user_config} + +cd /home/david +curl -z go1.4.2.linux-amd64.tar.gz -L -O https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz +tar -C /usr/local -zxf /home/david/go1.4.2.linux-amd64.tar.gz + +cat << END > /etc/profile.d/go.sh +export GOPATH=\\/home/david/go +export PATH=\\$GOPATH/bin:/usr/local/go/bin:\\$PATH +END + +cat << END > /etc/sudoers.d/go +Defaults env_keep += "GOPATH" +END + +#{hosts_config} +} + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/trusty64" + # We setup three nodes to be gluster hosts, and two gluster client to mount the volume + 3.times do |i| + id = i+1 + config.vm.define vm_name = "gfs-server-#{id}" do |config| + config.vm.hostname = vm_name + ip = "172.21.12.#{id+10}" + config.vm.network :private_network, ip: ip + config.vm.provision :shell, :inline => server_shell, :privileged => true + end + end + + 2.times do |i| + id = i+1 + config.vm.define vm_name = "gfs-client-#{id}" do |config| + config.ssh.forward_agent = true + config.vm.synced_folder ".", "/home/david/go/src/github.com/calavera/docker-volume-glusterfs", create: true + + config.vm.hostname = vm_name + ip = "172.21.12.#{id * 10}" + config.vm.network :private_network, ip: ip + config.vm.provision :shell, :inline => client_shell, :privileged => true + end + end +end diff --git a/driver.go b/driver.go new file mode 100644 index 0000000..9a72342 --- /dev/null +++ b/driver.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "os" + "os/exec" + "path/filepath" + "sync" + + "github.com/calavera/docker-volume-api" +) + +type volume struct { + name string + connections int +} + +type glusterfsDriver struct { + root string + servers []string + volumes map[string]*volume + m sync.Mutex +} + +func newGlusterfsDriver(root string, servers []string) glusterfsDriver { + return glusterfsDriver{ + root: root, + servers: servers, + volumes: map[string]*volume{}} +} + +func (d glusterfsDriver) Create(r volumeapi.VolumeRequest) volumeapi.VolumeResponse { + log.Printf("Creating volume %s\n", r.Name) + return volumeapi.VolumeResponse{} +} + +func (d glusterfsDriver) Remove(r volumeapi.VolumeRequest) volumeapi.VolumeResponse { + log.Printf("Removing volume %s\n", r.Name) + d.m.Lock() + defer d.m.Unlock() + m := d.mountpoint(r.Name) + + if s, ok := d.volumes[m]; ok { + if s.connections <= 1 { + delete(d.volumes, m) + } + } + return volumeapi.VolumeResponse{} +} + +func (d glusterfsDriver) Path(r volumeapi.VolumeRequest) volumeapi.VolumeResponse { + return volumeapi.VolumeResponse{Mountpoint: d.mountpoint(r.Name)} +} + +func (d glusterfsDriver) Mount(r volumeapi.VolumeRequest) volumeapi.VolumeResponse { + d.m.Lock() + defer d.m.Unlock() + m := d.mountpoint(r.Name) + log.Printf("Mounting volume %s on %s\n", r.Name, m) + + s, ok := d.volumes[m] + if ok && s.connections > 0 { + s.connections++ + return volumeapi.VolumeResponse{Mountpoint: m} + } + + fi, err := os.Lstat(m) + + if os.IsNotExist(err) { + if err := os.MkdirAll(m, 0755); err != nil { + return volumeapi.VolumeResponse{Err: err.Error()} + } + } else if err != nil { + return volumeapi.VolumeResponse{Err: err.Error()} + } + + if fi != nil && !fi.IsDir() { + return volumeapi.VolumeResponse{Err: fmt.Sprintf("%v already exist and it's not a directory", m)} + } + + if err := d.mountVolume(r.Name, m); err != nil { + return volumeapi.VolumeResponse{Err: err.Error()} + } + + d.volumes[m] = &volume{name: r.Name, connections: 1} + + return volumeapi.VolumeResponse{Mountpoint: m} +} + +func (d glusterfsDriver) Unmount(r volumeapi.VolumeRequest) volumeapi.VolumeResponse { + d.m.Lock() + defer d.m.Unlock() + m := d.mountpoint(r.Name) + log.Printf("Unmounting volume %s from %s\n", r.Name, m) + + if s, ok := d.volumes[m]; ok { + if s.connections == 1 { + if err := d.unmountVolume(m); err != nil { + return volumeapi.VolumeResponse{Err: err.Error()} + } + } + s.connections-- + } else { + return volumeapi.VolumeResponse{Err: fmt.Sprintf("Unable to find volume mounted on %s", m)} + } + + return volumeapi.VolumeResponse{} +} + +func (d *glusterfsDriver) mountpoint(name string) string { + return filepath.Join(d.root, name) +} + +func (d *glusterfsDriver) mountVolume(name, destination string) error { + server := d.servers[rand.Intn(len(d.servers))] + + cmd := fmt.Sprintf("glusterfs --log-level=DEBUG --volfile-id=%s --volfile-server=%s %s", name, server, destination) + if out, err := exec.Command("sh", "-c", cmd).CombinedOutput(); err != nil { + log.Println(string(out)) + return err + } + return nil +} + +func (d *glusterfsDriver) unmountVolume(target string) error { + cmd := fmt.Sprintf("umount %s", target) + if out, err := exec.Command("sh", "-c", cmd).CombinedOutput(); err != nil { + log.Println(string(out)) + return err + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9e4f294 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/calavera/docker-volume-api" +) + +var ( + serversList = flag.String("servers", "", "List of glusterfs servers") + root = flag.String("root", volumeapi.DefaultDockerRootDirectory, "Docker volumes root directory") +) + +func main() { + var Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) + flag.PrintDefaults() + } + + flag.Parse() + if len(*serversList) == 0 { + Usage() + os.Exit(1) + } + + servers := strings.Split(*serversList, ":") + + d := newGlusterfsDriver(*root, servers) + h := volumeapi.NewVolumeHandler(d) + fmt.Println("listening on :7878") + fmt.Println(h.ListenAndServe("tcp", ":7878", "")) +}