├── test ├── suites │ ├── deps.sh │ ├── devlxd.sh │ ├── serverconfig.sh │ ├── exec.sh │ ├── concurrent.sh │ ├── database_update.sh │ ├── init_interactive.sh │ ├── fdleak.sh │ ├── profiling.sh │ ├── pki.sh │ ├── security.sh │ ├── fuidshift.sh │ ├── image.sh │ ├── network.sh │ ├── image_prefer_cached.sh │ ├── init_preseed.sh │ ├── image_auto_update.sh │ ├── init_auto.sh │ └── template.sh ├── extras │ ├── speedtest_delete.sh │ └── speedtest_create.sh ├── backends │ ├── zfs.sh │ ├── lvm.sh │ ├── btrfs.sh │ └── dir.sh ├── README.md └── deps │ ├── schema1.sql │ ├── devlxd-client.go │ └── server.crt ├── fuidshift ├── Makefile └── main.go ├── lxd ├── storage_64bit.go ├── storage_32bit.go ├── main_ready.go ├── devlxd_gc.go ├── container_get.go ├── types │ └── devices_test.go ├── main_import.go ├── storage_zfs_utils.go ├── debug.go ├── db_patches.go ├── container_delete.go ├── main_shutdown.go ├── main_waitready.go ├── main_migratedumpsuccess.go ├── daemon_test.go ├── devlxd_gccgo.go ├── db_config.go ├── migrate.proto ├── main_callhook.go ├── util.go ├── cgroup.go ├── main_forkmigrate.go ├── main_forkstart.go ├── profiles_test.go ├── container_post.go ├── containers_get.go ├── daemon_images_test.go ├── main_activateifneeded.go ├── networks_iptables.go ├── db_storage_volumes.go ├── main_daemon.go ├── main_netcat.go ├── container_logs.go ├── container_patch.go └── storage_volumes_config.go ├── shared ├── i18n │ ├── i18n.go │ └── i18n_linux.go ├── osarch │ ├── architectures_others.go │ └── architectures_linux.go ├── util_windows.go ├── logging │ ├── log_windows.go │ ├── log_posix.go │ └── log.go ├── cmd │ ├── doc.go │ └── testing.go ├── version │ └── flex.go ├── util_unix.go ├── stringset_test.go ├── stringset.go ├── api │ ├── doc.go │ ├── operation.go │ ├── container_exec.go │ ├── certificate.go │ ├── profile.go │ ├── network.go │ ├── container_snapshot.go │ ├── status_code.go │ ├── server.go │ ├── response.go │ └── storage.go ├── ioprogress │ ├── reader.go │ ├── writer.go │ └── tracker.go ├── logger │ ├── format.go │ └── log.go ├── gnuflag │ └── export_test.go ├── cert_test.go ├── network_linux.go ├── termios │ ├── termios_windows.go │ └── termios.go ├── json.go └── util_test.go ├── .gitignore ├── doc ├── configuration.md ├── profiles.md ├── requirements.md ├── environment.md ├── daemon-behavior.md ├── backup.md ├── architectures.md ├── debugging.md ├── server.md ├── migration.md └── dev-lxd.md ├── AUTHORS ├── scripts └── vagrant │ ├── install-go.sh │ └── install-lxd.sh ├── .travis.yml ├── lxc ├── version.go ├── finger.go ├── utils_test.go ├── exec_windows.go ├── config │ ├── config.go │ ├── default.go │ └── file.go ├── restore.go ├── main_test.go ├── snapshot.go ├── manpage.go ├── monitor.go ├── help.go ├── move.go └── delete.go ├── Vagrantfile ├── client ├── simplestreams.go ├── lxd_operations.go ├── lxd_server.go ├── lxd_storage_pools.go ├── lxd_profiles.go ├── lxd_certificates.go ├── events.go ├── lxd_networks.go ├── lxd_events.go └── lxd_storage_volumes.go ├── .appveyor.yml ├── .github └── ISSUE_TEMPLATE.md ├── client_linux_test.go └── CONTRIBUTING.md /test/suites/deps.sh: -------------------------------------------------------------------------------- 1 | test_check_deps() { 2 | ! ldd "$(which lxc)" | grep -q liblxc 3 | } 4 | -------------------------------------------------------------------------------- /fuidshift/Makefile: -------------------------------------------------------------------------------- 1 | # we let go build figure out dependency changes 2 | .PHONY: fuidmap 3 | lxc: 4 | go build 5 | 6 | clean: 7 | -rm -f fuidshift 8 | -------------------------------------------------------------------------------- /lxd/storage_64bit.go: -------------------------------------------------------------------------------- 1 | // +build amd64 ppc64 ppc64le arm64 s390x 2 | 3 | package main 4 | 5 | const ( 6 | filesystemSuperMagicBtrfs = 0x9123683E 7 | ) 8 | -------------------------------------------------------------------------------- /shared/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package i18n 4 | 5 | // G returns the translated string 6 | func G(msgid string) string { 7 | return msgid 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | po/*.mo 3 | po/*.po~ 4 | lxd-*.tar.gz 5 | .vagrant 6 | test/deps/devlxd-client 7 | *~ 8 | tags 9 | 10 | # For Atom ctags 11 | .tags 12 | .tags1 13 | -------------------------------------------------------------------------------- /shared/osarch/architectures_others.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package osarch 4 | 5 | func ArchitectureGetLocal() (string, error) { 6 | return ArchitectureDefault, nil 7 | } 8 | -------------------------------------------------------------------------------- /shared/util_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package shared 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func GetOwnerMode(fInfo os.FileInfo) (os.FileMode, int, int) { 10 | return fInfo.Mode(), -1, -1 11 | } 12 | -------------------------------------------------------------------------------- /doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Current LXD stores configurations for a few components: 3 | 4 | - [Server](server.md) 5 | - [Containers](containers.md) 6 | - [Network](networks.md) 7 | - [Profiles](profiles.md) 8 | - [Storage](storage.md) 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Unless mentioned otherwise in a specific file's header, all code in this 2 | project is released under the Apache 2.0 license. 3 | 4 | The list of authors and contributors can be retrieved from the git 5 | commit history and in some cases, the file headers. 6 | -------------------------------------------------------------------------------- /lxd/storage_32bit.go: -------------------------------------------------------------------------------- 1 | // +build 386 arm ppc s390 2 | 3 | package main 4 | 5 | const ( 6 | /* This is really 0x9123683E, go wants us to give it in signed form 7 | * since we use it as a signed constant. */ 8 | filesystemSuperMagicBtrfs = -1859950530 9 | ) 10 | -------------------------------------------------------------------------------- /shared/logging/log_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package logging 4 | 5 | import ( 6 | log "gopkg.in/inconshreveable/log15.v2" 7 | ) 8 | 9 | // getSystemHandler on Windows does nothing. 10 | func getSystemHandler(syslog string, debug bool, format log.Format) log.Handler { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /shared/i18n/i18n_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package i18n 4 | 5 | import ( 6 | "github.com/gosexy/gettext" 7 | ) 8 | 9 | // G returns the translated string 10 | func G(msgid string) string { 11 | return gettext.DGettext("lxd", msgid) 12 | } 13 | 14 | func init() { 15 | gettext.SetLocale(gettext.LC_ALL, "") 16 | } 17 | -------------------------------------------------------------------------------- /shared/cmd/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The package cmd implements a simple abstraction around a "sub-command" for 4 | a main executable (e.g. "lxd init", where "init" is the sub-command). 5 | 6 | It is designed to make unit-testing easier, since OS-specific parts like 7 | standard in/out can be set in tests. 8 | 9 | */ 10 | 11 | package cmd 12 | -------------------------------------------------------------------------------- /shared/version/flex.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version contains the LXD version number 4 | var Version = "2.14" 5 | 6 | // UserAgent contains a string suitable as a user-agent 7 | var UserAgent = "LXD " + Version 8 | 9 | // APIVersion contains the API base version. Only bumped for backward incompatible changes. 10 | var APIVersion = "1.0" 11 | -------------------------------------------------------------------------------- /shared/util_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package shared 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func GetOwnerMode(fInfo os.FileInfo) (os.FileMode, int, int) { 11 | mode := fInfo.Mode() 12 | uid := int(fInfo.Sys().(*syscall.Stat_t).Uid) 13 | gid := int(fInfo.Sys().(*syscall.Stat_t).Gid) 14 | return mode, uid, gid 15 | } 16 | -------------------------------------------------------------------------------- /lxd/main_ready.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/lxc/lxd/client" 5 | ) 6 | 7 | func cmdReady() error { 8 | c, err := lxd.ConnectLXDUnix("", nil) 9 | if err != nil { 10 | return err 11 | } 12 | 13 | _, _, err = c.RawQuery("PUT", "/internal/ready", nil, "") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /lxd/devlxd_gc.go: -------------------------------------------------------------------------------- 1 | // +build gc 2 | 3 | package main 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func getUcred(fd int) (uint32, uint32, int32, error) { 10 | cred, err := syscall.GetsockoptUcred(fd, syscall.SOL_SOCKET, syscall.SO_PEERCRED) 11 | if err != nil { 12 | return 0, 0, -1, err 13 | } 14 | 15 | return cred.Uid, cred.Gid, cred.Pid, nil 16 | } 17 | -------------------------------------------------------------------------------- /lxd/container_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | func containerGet(d *Daemon, r *http.Request) Response { 10 | name := mux.Vars(r)["name"] 11 | c, err := containerLoadByName(d, name) 12 | if err != nil { 13 | return SmartError(err) 14 | } 15 | 16 | state, etag, err := c.Render() 17 | if err != nil { 18 | return SmartError(err) 19 | } 20 | 21 | return SyncResponseETag(true, state, etag) 22 | } 23 | -------------------------------------------------------------------------------- /shared/osarch/architectures_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package osarch 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func ArchitectureGetLocal() (string, error) { 10 | uname := syscall.Utsname{} 11 | if err := syscall.Uname(&uname); err != nil { 12 | return ArchitectureDefault, err 13 | } 14 | 15 | architectureName := "" 16 | for _, c := range uname.Machine { 17 | if c == 0 { 18 | break 19 | } 20 | architectureName += string(byte(c)) 21 | } 22 | 23 | return architectureName, nil 24 | } 25 | -------------------------------------------------------------------------------- /shared/stringset_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStringSetSubset(t *testing.T) { 8 | ss := NewStringSet([]string{"one", "two"}) 9 | 10 | if !ss.IsSubset(ss) { 11 | t.Error("subests wrong") 12 | return 13 | } 14 | 15 | if !ss.IsSubset(NewStringSet([]string{"one", "two", "three"})) { 16 | t.Error("subsets wrong") 17 | return 18 | } 19 | 20 | if ss.IsSubset(NewStringSet([]string{"four"})) { 21 | t.Error("subests wrong") 22 | return 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/vagrant/install-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | export DEBIAN_FRONTEND=noninteractive 5 | 6 | which add-apt-repository || (sudo apt-get update ; sudo apt-get install -y software-properties-common) 7 | sudo add-apt-repository ppa:ubuntu-lxc/lxd-git-master 8 | sudo apt-get update 9 | which go || sudo apt-get install -y golang 10 | 11 | [ -e $HOME/go ] || mkdir -p $HOME/go 12 | 13 | cat << 'EOF' | sudo tee /etc/profile.d/S99go.sh 14 | export GOPATH=$HOME/go 15 | export PATH=$PATH:$GOPATH/bin 16 | EOF 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - osx 5 | 6 | go: 7 | - 1.6 8 | - 1.7 9 | - tip 10 | 11 | matrix: 12 | fast_finish: true 13 | allow_failures: 14 | - go: tip 15 | 16 | install: 17 | - "mkdir -p $GOPATH/github.com/lxc" 18 | - "rsync -az ${TRAVIS_BUILD_DIR}/ $HOME/gopath/src/github.com/lxc/lxd/" 19 | 20 | script: 21 | - "make client" 22 | - "go test ./" 23 | - "go test ./client" 24 | - "go test ./lxc" 25 | - "go test ./shared" 26 | 27 | notifications: 28 | webhooks: https://linuxcontainers.org/webhook-lxcbot/ 29 | -------------------------------------------------------------------------------- /lxd/types/devices_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSortableDevices(t *testing.T) { 9 | devices := Devices{ 10 | "1": Device{"type": "nic"}, 11 | "3": Device{"type": "disk", "path": "/foo/bar"}, 12 | "4": Device{"type": "disk", "path": "/foo"}, 13 | "2": Device{"type": "nic"}, 14 | } 15 | 16 | expected := []string{"1", "2", "4", "3"} 17 | 18 | result := devices.DeviceNames() 19 | if !reflect.DeepEqual(result, expected) { 20 | t.Error("devices sorted incorrectly") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/suites/devlxd.sh: -------------------------------------------------------------------------------- 1 | test_devlxd() { 2 | ensure_import_testimage 3 | 4 | # shellcheck disable=SC2164 5 | cd "${TEST_DIR}" 6 | go build -tags netgo -a -installsuffix devlxd ../deps/devlxd-client.go 7 | # shellcheck disable=SC2164 8 | cd - 9 | 10 | lxc launch testimage devlxd 11 | 12 | lxc file push "${TEST_DIR}/devlxd-client" devlxd/bin/ 13 | 14 | lxc exec devlxd chmod +x /bin/devlxd-client 15 | 16 | lxc config set devlxd user.foo bar 17 | lxc exec devlxd devlxd-client user.foo | grep bar 18 | 19 | lxc delete devlxd --force 20 | } 21 | -------------------------------------------------------------------------------- /shared/logging/log_posix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin 2 | 3 | package logging 4 | 5 | import ( 6 | log "gopkg.in/inconshreveable/log15.v2" 7 | ) 8 | 9 | // getSystemHandler on Linux writes messages to syslog. 10 | func getSystemHandler(syslog string, debug bool, format log.Format) log.Handler { 11 | // SyslogHandler 12 | if syslog != "" { 13 | if !debug { 14 | return log.LvlFilterHandler( 15 | log.LvlInfo, 16 | log.Must.SyslogHandler(syslog, format), 17 | ) 18 | } 19 | 20 | return log.Must.SyslogHandler(syslog, format) 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /shared/stringset.go: -------------------------------------------------------------------------------- 1 | // That this code needs to exist is kind of dumb, but I'm not sure how else to 2 | // do it. 3 | package shared 4 | 5 | type StringSet map[string]bool 6 | 7 | func (ss StringSet) IsSubset(oss StringSet) bool { 8 | for k := range map[string]bool(ss) { 9 | if _, ok := map[string]bool(oss)[k]; !ok { 10 | return false 11 | } 12 | } 13 | 14 | return true 15 | } 16 | 17 | func NewStringSet(strings []string) StringSet { 18 | ret := map[string]bool{} 19 | for _, s := range strings { 20 | ret[s] = true 21 | } 22 | 23 | return StringSet(ret) 24 | } 25 | -------------------------------------------------------------------------------- /shared/api/doc.go: -------------------------------------------------------------------------------- 1 | // Package api contains Go structs for all LXD API objects 2 | // 3 | // Overview 4 | // 5 | // This package has Go structs for every API object, all the various 6 | // structs are named after the object they represent and some variations of 7 | // those structs exist for initial object creation, object update and 8 | // object retrieval. 9 | // 10 | // A few convenience functions are also tied to those structs which let 11 | // you convert between the various strucs for a given object and also query 12 | // some of the more complex metadata that LXD can export. 13 | package api 14 | -------------------------------------------------------------------------------- /lxd/main_import.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lxc/lxd/client" 6 | ) 7 | 8 | func cmdImport(args []string) error { 9 | if len(args) < 2 { 10 | return fmt.Errorf("please specify a container to import") 11 | } 12 | name := args[1] 13 | req := map[string]interface{}{ 14 | "name": name, 15 | "force": *argForce, 16 | } 17 | 18 | c, err := lxd.ConnectLXDUnix("", nil) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | _, _, err = c.RawQuery("POST", "/internal/containers", req, "") 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /lxd/storage_zfs_utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared" 8 | ) 9 | 10 | // zfsPoolVolumeCreate creates a ZFS dataset with a set of given properties. 11 | func zfsPoolVolumeCreate(dataset string, properties ...string) (string, error) { 12 | p := strings.Join(properties, ",") 13 | return shared.RunCommand("zfs", "create", "-o", p, "-p", dataset) 14 | } 15 | 16 | func zfsPoolVolumeSet(dataset string, key string, value string) (string, error) { 17 | return shared.RunCommand("zfs", 18 | "set", 19 | fmt.Sprintf("%s=%s", key, value), 20 | dataset) 21 | } 22 | -------------------------------------------------------------------------------- /test/extras/speedtest_delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MYDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | CIMAGE="testimage" 5 | CNAME="speedtest" 6 | 7 | count=${1} 8 | if [ "x${count}" == "x" ]; then 9 | echo "USAGE: ${0} 10" 10 | echo "This deletes 10 busybox containers" 11 | exit 1 12 | fi 13 | 14 | if [ "x${2}" != "xnotime" ]; then 15 | time ${0} ${count} notime 16 | exit 0 17 | fi 18 | 19 | PIDS="" 20 | for c in $(seq 1 $count); do 21 | lxc delete "${CNAME}${c}" 2>&1 & 22 | PIDS="$PIDS $!" 23 | done 24 | 25 | for pid in $PIDS; do 26 | wait $pid 27 | done 28 | 29 | echo -e "\nRun completed" 30 | -------------------------------------------------------------------------------- /shared/ioprogress/reader.go: -------------------------------------------------------------------------------- 1 | package ioprogress 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // ProgressReader is a wrapper around ReadCloser which allows for progress tracking 8 | type ProgressReader struct { 9 | io.ReadCloser 10 | Tracker *ProgressTracker 11 | } 12 | 13 | // Read in ProgressReader is the same as io.Read 14 | func (pt *ProgressReader) Read(p []byte) (int, error) { 15 | // Do normal reader tasks 16 | n, err := pt.ReadCloser.Read(p) 17 | 18 | // Do the actual progress tracking 19 | if pt.Tracker != nil { 20 | pt.Tracker.total += int64(n) 21 | pt.Tracker.update(n) 22 | } 23 | 24 | return n, err 25 | } 26 | -------------------------------------------------------------------------------- /shared/ioprogress/writer.go: -------------------------------------------------------------------------------- 1 | package ioprogress 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // ProgressWriter is a wrapper around WriteCloser which allows for progress tracking 8 | type ProgressWriter struct { 9 | io.WriteCloser 10 | Tracker *ProgressTracker 11 | } 12 | 13 | // Write in ProgressWriter is the same as io.Write 14 | func (pt *ProgressWriter) Write(p []byte) (int, error) { 15 | // Do normal writer tasks 16 | n, err := pt.WriteCloser.Write(p) 17 | 18 | // Do the actual progress tracking 19 | if pt.Tracker != nil { 20 | pt.Tracker.total += int64(n) 21 | pt.Tracker.update(n) 22 | } 23 | 24 | return n, err 25 | } 26 | -------------------------------------------------------------------------------- /test/suites/serverconfig.sh: -------------------------------------------------------------------------------- 1 | test_server_config() { 2 | LXD_SERVERCONFIG_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 3 | spawn_lxd "${LXD_SERVERCONFIG_DIR}" true 4 | 5 | ensure_has_localhost_remote "${LXD_ADDR}" 6 | lxc config set core.trust_password 123456 7 | 8 | config=$(lxc config show) 9 | echo "${config}" | grep -q "trust_password" 10 | echo "${config}" | grep -q -v "123456" 11 | 12 | lxc config unset core.trust_password 13 | lxc config show | grep -q -v "trust_password" 14 | 15 | # test untrusted server GET 16 | my_curl -X GET "https://$(cat "${LXD_SERVERCONFIG_DIR}/lxd.addr")/1.0" | grep -v -q environment 17 | } 18 | -------------------------------------------------------------------------------- /shared/logger/format.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | ) 8 | 9 | // Pretty will attempt to convert any Go structure into a string suitable for logging 10 | func Pretty(input interface{}) string { 11 | pretty, err := json.MarshalIndent(input, "\t", "\t") 12 | if err != nil { 13 | return fmt.Sprintf("%v", input) 14 | } 15 | 16 | return fmt.Sprintf("\n\t%s", pretty) 17 | } 18 | 19 | // GetStack will convert the Go stack into a string suitable for logging 20 | func GetStack() string { 21 | buf := make([]byte, 1<<16) 22 | runtime.Stack(buf, true) 23 | 24 | return fmt.Sprintf("\n\t%s", buf) 25 | } 26 | -------------------------------------------------------------------------------- /lxd/debug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "runtime/pprof" 7 | "syscall" 8 | 9 | "github.com/lxc/lxd/shared/logger" 10 | ) 11 | 12 | func doMemDump(memProfile string) { 13 | f, err := os.Create(memProfile) 14 | if err != nil { 15 | logger.Debugf("Error opening memory profile file '%s': %s", err) 16 | return 17 | } 18 | pprof.WriteHeapProfile(f) 19 | f.Close() 20 | } 21 | 22 | func memProfiler(memProfile string) { 23 | ch := make(chan os.Signal) 24 | signal.Notify(ch, syscall.SIGUSR1) 25 | for { 26 | sig := <-ch 27 | logger.Debugf("Received '%s signal', dumping memory.", sig) 28 | doMemDump(memProfile) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /doc/profiles.md: -------------------------------------------------------------------------------- 1 | # Profiles 2 | Profiles can store any configuration that a container can (key/value or 3 | devices) and any number of profiles can be applied to a container. 4 | 5 | Profiles are applied in the order they are specified so the last profile to 6 | specify a specific key wins. 7 | 8 | In any case, resource-specific configuration always overrides that coming from 9 | the profiles. 10 | 11 | If not present, LXD will create a "default" profile. 12 | 13 | The "default" profile is set for any new container created which doesn't 14 | specify a different profiles list. 15 | 16 | 17 | See [container configuration](containers.md) for valid configuration options. 18 | -------------------------------------------------------------------------------- /lxc/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lxc/lxd" 7 | "github.com/lxc/lxd/shared/i18n" 8 | "github.com/lxc/lxd/shared/version" 9 | ) 10 | 11 | type versionCmd struct{} 12 | 13 | func (c *versionCmd) showByDefault() bool { 14 | return false 15 | } 16 | 17 | func (c *versionCmd) usage() string { 18 | return i18n.G( 19 | `Usage: lxc version 20 | 21 | Print the version number of this client tool.`) 22 | } 23 | 24 | func (c *versionCmd) flags() { 25 | } 26 | 27 | func (c *versionCmd) run(_ *lxd.Config, args []string) error { 28 | if len(args) > 0 { 29 | return errArgs 30 | } 31 | fmt.Println(version.Version) 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure('2') do |config| 2 | # grab Ubuntu 14.04 boxcutter image: https://atlas.hashicorp.com/boxcutter 3 | config.vm.box = "boxcutter/ubuntu1404" # Ubuntu 14.04 4 | 5 | # fix issues with slow dns https://www.virtualbox.org/ticket/13002 6 | config.vm.provider :virtualbox do |vb, override| 7 | vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"] 8 | end 9 | 10 | config.vm.network "forwarded_port", guest: 443, host: 8443 11 | config.vm.provision :shell, :privileged => false, :path => "scripts/vagrant/install-go.sh" 12 | config.vm.provision :shell, :privileged => false, :path => "scripts/vagrant/install-lxd.sh" 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/suites/exec.sh: -------------------------------------------------------------------------------- 1 | test_concurrent_exec() { 2 | if [ -z "${LXD_CONCURRENT:-}" ]; then 3 | echo "==> SKIP: LXD_CONCURRENT isn't set" 4 | return 5 | fi 6 | 7 | ensure_import_testimage 8 | 9 | name=x1 10 | lxc launch testimage x1 11 | lxc list ${name} | grep RUNNING 12 | 13 | exec_container() { 14 | echo "abc${1}" | lxc exec "${name}" -- cat | grep abc 15 | } 16 | 17 | PIDS="" 18 | for i in $(seq 1 50); do 19 | exec_container "${i}" > "${LXD_DIR}/exec-${i}.out" 2>&1 & 20 | PIDS="${PIDS} $!" 21 | done 22 | 23 | for pid in ${PIDS}; do 24 | wait "${pid}" 25 | done 26 | 27 | lxc stop "${name}" --force 28 | lxc delete "${name}" 29 | } 30 | -------------------------------------------------------------------------------- /test/backends/zfs.sh: -------------------------------------------------------------------------------- 1 | zfs_setup() { 2 | # shellcheck disable=2039 3 | local LXD_DIR 4 | 5 | LXD_DIR=$1 6 | 7 | echo "==> Setting up ZFS backend in ${LXD_DIR}" 8 | } 9 | 10 | zfs_configure() { 11 | # shellcheck disable=2039 12 | local LXD_DIR 13 | 14 | LXD_DIR=$1 15 | 16 | echo "==> Configuring ZFS backend in ${LXD_DIR}" 17 | 18 | lxc storage create "lxdtest-$(basename "${LXD_DIR}")" zfs size=100GB 19 | lxc profile device add default root disk path="/" pool="lxdtest-$(basename "${LXD_DIR}")" 20 | } 21 | 22 | zfs_teardown() { 23 | # shellcheck disable=2039 24 | local LXD_DIR 25 | 26 | LXD_DIR=$1 27 | 28 | echo "==> Tearing down ZFS backend in ${LXD_DIR}" 29 | } 30 | -------------------------------------------------------------------------------- /test/backends/lvm.sh: -------------------------------------------------------------------------------- 1 | lvm_setup() { 2 | # shellcheck disable=2039 3 | local LXD_DIR 4 | 5 | LXD_DIR=$1 6 | 7 | echo "==> Setting up lvm backend in ${LXD_DIR}" 8 | } 9 | 10 | lvm_configure() { 11 | # shellcheck disable=2039 12 | local LXD_DIR 13 | 14 | LXD_DIR=$1 15 | 16 | echo "==> Configuring lvm backend in ${LXD_DIR}" 17 | 18 | lxc storage create "lxdtest-$(basename "${LXD_DIR}")" lvm volume.size=25MB 19 | lxc profile device add default root disk path="/" pool="lxdtest-$(basename "${LXD_DIR}")" 20 | } 21 | 22 | lvm_teardown() { 23 | # shellcheck disable=2039 24 | local LXD_DIR 25 | 26 | LXD_DIR=$1 27 | 28 | echo "==> Tearing down lvm backend in ${LXD_DIR}" 29 | } 30 | -------------------------------------------------------------------------------- /test/backends/btrfs.sh: -------------------------------------------------------------------------------- 1 | btrfs_setup() { 2 | # shellcheck disable=2039 3 | local LXD_DIR 4 | 5 | LXD_DIR=$1 6 | 7 | echo "==> Setting up btrfs backend in ${LXD_DIR}" 8 | } 9 | 10 | btrfs_configure() { 11 | # shellcheck disable=2039 12 | local LXD_DIR 13 | 14 | LXD_DIR=$1 15 | 16 | lxc storage create "lxdtest-$(basename "${LXD_DIR}")" btrfs size=100GB 17 | lxc profile device add default root disk path="/" pool="lxdtest-$(basename "${LXD_DIR}")" 18 | 19 | echo "==> Configuring btrfs backend in ${LXD_DIR}" 20 | } 21 | 22 | btrfs_teardown() { 23 | # shellcheck disable=2039 24 | local LXD_DIR 25 | 26 | LXD_DIR=$1 27 | 28 | echo "==> Tearing down btrfs backend in ${LXD_DIR}" 29 | } 30 | -------------------------------------------------------------------------------- /lxd/db_patches.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | func dbPatches(db *sql.DB) ([]string, error) { 9 | inargs := []interface{}{} 10 | outfmt := []interface{}{""} 11 | 12 | query := fmt.Sprintf("SELECT name FROM patches") 13 | result, err := dbQueryScan(db, query, inargs, outfmt) 14 | if err != nil { 15 | return []string{}, err 16 | } 17 | 18 | response := []string{} 19 | for _, r := range result { 20 | response = append(response, r[0].(string)) 21 | } 22 | 23 | return response, nil 24 | } 25 | 26 | func dbPatchesMarkApplied(db *sql.DB, patch string) error { 27 | stmt := `INSERT INTO patches (name, applied_at) VALUES (?, strftime("%s"));` 28 | _, err := db.Exec(stmt, patch) 29 | return err 30 | } 31 | -------------------------------------------------------------------------------- /shared/gnuflag/export_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gnuflag 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // Additional routines compiled into the package only during testing. 12 | 13 | // ResetForTesting clears all flag state and sets the usage function as directed. 14 | // After calling ResetForTesting, parse errors in flag handling will not 15 | // exit the program. 16 | func ResetForTesting(usage func()) { 17 | commandLine = NewFlagSet(os.Args[0], ContinueOnError) 18 | Usage = usage 19 | } 20 | 21 | // CommandLine returns the default FlagSet. 22 | func CommandLine() *FlagSet { 23 | return commandLine 24 | } 25 | -------------------------------------------------------------------------------- /client/simplestreams.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/lxc/lxd/shared/simplestreams" 7 | ) 8 | 9 | // ProtocolSimpleStreams implements a SimpleStreams API client 10 | type ProtocolSimpleStreams struct { 11 | ssClient *simplestreams.SimpleStreams 12 | 13 | http *http.Client 14 | httpHost string 15 | httpUserAgent string 16 | httpCertificate string 17 | } 18 | 19 | // GetConnectionInfo returns the basic connection information used to interact with the server 20 | func (r *ProtocolSimpleStreams) GetConnectionInfo() (*ConnectionInfo, error) { 21 | info := ConnectionInfo{} 22 | info.Addresses = []string{r.httpHost} 23 | info.Certificate = r.httpCertificate 24 | info.Protocol = "simplestreams" 25 | 26 | return &info, nil 27 | } 28 | -------------------------------------------------------------------------------- /lxd/container_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | func containerDelete(d *Daemon, r *http.Request) Response { 11 | name := mux.Vars(r)["name"] 12 | c, err := containerLoadByName(d, name) 13 | if err != nil { 14 | return SmartError(err) 15 | } 16 | 17 | if c.IsRunning() { 18 | return BadRequest(fmt.Errorf("container is running")) 19 | } 20 | 21 | rmct := func(op *operation) error { 22 | return c.Delete() 23 | } 24 | 25 | resources := map[string][]string{} 26 | resources["containers"] = []string{name} 27 | 28 | op, err := operationCreate(operationClassTask, resources, nil, rmct, nil, nil) 29 | if err != nil { 30 | return InternalError(err) 31 | } 32 | 33 | return OperationResponse(op) 34 | } 35 | -------------------------------------------------------------------------------- /test/suites/concurrent.sh: -------------------------------------------------------------------------------- 1 | test_concurrent() { 2 | if [ -z "${LXD_CONCURRENT:-}" ]; then 3 | echo "==> SKIP: LXD_CONCURRENT isn't set" 4 | return 5 | fi 6 | 7 | ensure_import_testimage 8 | 9 | spawn_container() { 10 | set -e 11 | 12 | name=concurrent-${1} 13 | 14 | lxc launch testimage "${name}" 15 | lxc info "${name}" | grep Running 16 | echo abc | lxc exec "${name}" -- cat | grep abc 17 | lxc stop "${name}" --force 18 | lxc delete "${name}" 19 | } 20 | 21 | PIDS="" 22 | 23 | for id in $(seq $(($(find /sys/bus/cpu/devices/ -type l | wc -l)*8))); do 24 | spawn_container "${id}" 2>&1 | tee "${LXD_DIR}/lxc-${id}.out" & 25 | PIDS="${PIDS} $!" 26 | done 27 | 28 | for pid in ${PIDS}; do 29 | wait "${pid}" 30 | done 31 | 32 | ! lxc list | grep -q concurrent 33 | } 34 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}.{build}' 2 | clone_folder: c:\gopath\src\github.com\lxc\lxd 3 | environment: 4 | GOPATH: c:\gopath 5 | 6 | install: 7 | - cmd: |- 8 | echo %PATH% 9 | echo %GOPATH% 10 | set PATH=%GOPATH%\bin;c:\go\bin;%PATH% 11 | go version 12 | go env 13 | 14 | build_script: 15 | - cmd: |- 16 | go get -t -v -d ./... 17 | go install -v ./lxc 18 | 19 | test_script: 20 | - cmd: |- 21 | go test -v ./ 22 | go test -v ./client 23 | go test -v ./lxc 24 | go test -v ./shared 25 | 26 | after_test: 27 | # powershell capture command output into environment variable 28 | - ps: $env:VERSION = lxc version 29 | - echo %VERSION% 30 | # pack lxc as an artifact for upload 31 | - 7z a lxc-%VERSION%-windows-amd64.zip c:\gopath\bin\lxc.exe 32 | 33 | artifacts: 34 | - path: "*.zip" 35 | -------------------------------------------------------------------------------- /shared/api/operation.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Operation represents a LXD background operation 8 | type Operation struct { 9 | ID string `json:"id" yaml:"id"` 10 | Class string `json:"class" yaml:"class"` 11 | CreatedAt time.Time `json:"created_at" yaml:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"` 13 | Status string `json:"status" yaml:"status"` 14 | StatusCode StatusCode `json:"status_code" yaml:"status_code"` 15 | Resources map[string][]string `json:"resources" yaml:"resources"` 16 | Metadata map[string]interface{} `json:"metadata" yaml:"metadata"` 17 | MayCancel bool `json:"may_cancel" yaml:"may_cancel"` 18 | Err string `json:"err" yaml:"err"` 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | The template below is mostly useful for bug reports and support questions. 2 | Feel free to remove anything which doesn't apply to you and add more information where it makes sense. 3 | 4 | # Required information 5 | 6 | * Distribution: 7 | * Distribution version: 8 | * The output of "lxc info" or if that fails: 9 | * Kernel version: 10 | * LXC version: 11 | * LXD version: 12 | * Storage backend in use: 13 | 14 | # Issue description 15 | 16 | A brief description of what failed or what could be improved. 17 | 18 | # Steps to reproduce 19 | 20 | 1. Step one 21 | 2. Step two 22 | 3. Step three 23 | 24 | # Information to attach 25 | 26 | - [ ] any relevant kernel output (dmesg) 27 | - [ ] container log (lxc info NAME --show-log) 28 | - [ ] main daemon log (/var/log/lxd.log) 29 | - [ ] output of the client with --debug 30 | - [ ] output of the daemon with --debug 31 | -------------------------------------------------------------------------------- /lxd/main_shutdown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/lxc/lxd/client" 8 | ) 9 | 10 | func cmdShutdown() error { 11 | var timeout int 12 | 13 | if *argTimeout == -1 { 14 | timeout = 60 15 | } else { 16 | timeout = *argTimeout 17 | } 18 | 19 | c, err := lxd.ConnectLXDUnix("", nil) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | _, _, err = c.RawQuery("PUT", "/internal/shutdown", nil, "") 25 | if err != nil { 26 | return err 27 | } 28 | 29 | chMonitor := make(chan bool, 1) 30 | go func() { 31 | monitor, err := c.GetEvents() 32 | if err != nil { 33 | close(chMonitor) 34 | return 35 | } 36 | 37 | monitor.Wait() 38 | close(chMonitor) 39 | }() 40 | 41 | select { 42 | case <-chMonitor: 43 | break 44 | case <-time.After(time.Second * time.Duration(timeout)): 45 | return fmt.Errorf("LXD still running after %ds timeout.", timeout) 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /lxd/main_waitready.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/lxc/lxd/client" 8 | ) 9 | 10 | func cmdWaitReady() error { 11 | var timeout int 12 | 13 | if *argTimeout == -1 { 14 | timeout = 15 15 | } else { 16 | timeout = *argTimeout 17 | } 18 | 19 | finger := make(chan error, 1) 20 | go func() { 21 | for { 22 | c, err := lxd.ConnectLXDUnix("", nil) 23 | if err != nil { 24 | time.Sleep(500 * time.Millisecond) 25 | continue 26 | } 27 | 28 | _, _, err = c.RawQuery("GET", "/internal/ready", nil, "") 29 | if err != nil { 30 | time.Sleep(500 * time.Millisecond) 31 | continue 32 | } 33 | 34 | finger <- nil 35 | return 36 | } 37 | }() 38 | 39 | select { 40 | case <-finger: 41 | break 42 | case <-time.After(time.Second * time.Duration(timeout)): 43 | return fmt.Errorf("LXD still not running after %ds timeout.", timeout) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /doc/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | ## Go 3 | 4 | LXD requires Go 1.5 or higher. 5 | Both the golang and gccgo compilers are supported. 6 | 7 | ## Kernel requirements 8 | The minimum supported kernel version is 3.13. 9 | 10 | LXD requires a kernel with support for: 11 | * Namespaces (pid, net, uts, ipc and mount) 12 | * Seccomp 13 | 14 | The following optional features also require extra kernel options: 15 | * Namespaces (user and cgroup) 16 | * AppArmor (including Ubuntu patch for mount mediation) 17 | * Control Groups (blkio, cpuset, devices, memory, pids and net\_prio) 18 | * CRIU (exact details to be found with CRIU upstream) 19 | 20 | As well as any other kernel feature required by the LXC version in use. 21 | 22 | ## LXC 23 | LXD requires LXC 2.0.0 or higher with the following build options: 24 | * apparmor (if using LXD's apparmor support) 25 | * seccomp 26 | 27 | To run recent version of various distributions, including Ubuntu, LXCFS 28 | should also be installed. 29 | -------------------------------------------------------------------------------- /lxd/main_migratedumpsuccess.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/lxc/lxd/client" 8 | "github.com/lxc/lxd/shared/api" 9 | ) 10 | 11 | func cmdMigrateDumpSuccess(args []string) error { 12 | if len(args) != 3 { 13 | return fmt.Errorf("bad migrate dump success args %s", args) 14 | } 15 | 16 | c, err := lxd.ConnectLXDUnix("", nil) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | conn, err := c.RawWebsocket(fmt.Sprintf("/1.0/operations/%s/websocket?%s", args[1], url.Values{"secret": []string{args[2]}})) 22 | if err != nil { 23 | return err 24 | } 25 | conn.Close() 26 | 27 | resp, _, err := c.RawQuery("GET", fmt.Sprintf("/1.0/operations/%s/wait", args[1]), nil, "") 28 | if err != nil { 29 | return err 30 | } 31 | 32 | op, err := resp.MetadataAsOperation() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if op.StatusCode == api.Success { 38 | return nil 39 | } 40 | 41 | return fmt.Errorf(op.Err) 42 | } 43 | -------------------------------------------------------------------------------- /scripts/vagrant/install-lxd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | export DEBIAN_FRONTEND=noninteractive 5 | 6 | # install runtime dependencies 7 | sudo apt-get -y install xz-utils tar acl curl gettext \ 8 | jq sqlite3 9 | 10 | # install build dependencies 11 | sudo apt-get -y install lxc lxc-dev mercurial git pkg-config \ 12 | protobuf-compiler golang-goprotobuf-dev squashfs-tools 13 | 14 | # setup env 15 | [ -e uid_gid_setup ] || \ 16 | echo "root:1000000:65536" | sudo tee -a /etc/subuid /etc/subgid && \ 17 | touch uid_gid_setup 18 | 19 | 20 | go get github.com/lxc/lxd 21 | cd $GOPATH/src/github.com/lxc/lxd 22 | go get -v -d ./... 23 | make 24 | 25 | 26 | cat << 'EOF' | sudo tee /etc/init/lxd.conf 27 | description "LXD daemon" 28 | author "John Brooker" 29 | 30 | start on filesystem or runlevel [2345] 31 | stop on shutdown 32 | 33 | script 34 | 35 | exec /home/vagrant/go/bin/lxd --group vagrant 36 | 37 | end script 38 | 39 | EOF 40 | 41 | sudo service lxd start 42 | -------------------------------------------------------------------------------- /test/backends/dir.sh: -------------------------------------------------------------------------------- 1 | # Nothing need be done for the dir backed, but we still need some functions. 2 | # This file can also serve as a skel file for what needs to be done to 3 | # implement a new backend. 4 | 5 | # Any necessary backend-specific setup 6 | dir_setup() { 7 | # shellcheck disable=2039 8 | local LXD_DIR 9 | 10 | LXD_DIR=$1 11 | 12 | echo "==> Setting up directory backend in ${LXD_DIR}" 13 | } 14 | 15 | # Do the API voodoo necessary to configure LXD to use this backend 16 | dir_configure() { 17 | # shellcheck disable=2039 18 | local LXD_DIR 19 | 20 | LXD_DIR=$1 21 | 22 | echo "==> Configuring directory backend in ${LXD_DIR}" 23 | 24 | lxc storage create "lxdtest-$(basename "${LXD_DIR}")" dir 25 | lxc profile device add default root disk path="/" pool="lxdtest-$(basename "${LXD_DIR}")" 26 | } 27 | 28 | dir_teardown() { 29 | # shellcheck disable=2039 30 | local LXD_DIR 31 | 32 | LXD_DIR=$1 33 | 34 | echo "==> Tearing down directory backend in ${LXD_DIR}" 35 | } 36 | -------------------------------------------------------------------------------- /test/extras/speedtest_create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MYDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | CIMAGE="testimage" 5 | CNAME="speedtest" 6 | 7 | count=${1} 8 | if [ "x${count}" == "x" ]; then 9 | echo "USAGE: ${0} 10" 10 | echo "This creates 10 busybox containers" 11 | exit 1 12 | fi 13 | 14 | if [ "x${2}" != "xnotime" ]; then 15 | time ${0} ${count} notime 16 | exit 0 17 | fi 18 | 19 | ${MYDIR}/deps/import-busybox --alias busybox 20 | 21 | PIDS="" 22 | for c in $(seq 1 $count); do 23 | lxc init busybox "${CNAME}${c}" 2>&1 & 24 | PIDS="$PIDS $!" 25 | done 26 | 27 | for pid in $PIDS; do 28 | wait $pid 29 | done 30 | 31 | echo -e "\nlxc list: All shutdown" 32 | time lxc list 1>/dev/null 33 | 34 | PIDS="" 35 | for c in $(seq 1 $count); do 36 | lxc start "${CNAME}${c}" 2>&1 & 37 | PIDS="$PIDS $!" 38 | done 39 | 40 | for pid in $PIDS; do 41 | wait $pid 42 | done 43 | 44 | echo -e "\nlxc list: All started" 45 | time lxc list 1>/dev/null 46 | 47 | echo -e "\nRun completed" 48 | -------------------------------------------------------------------------------- /shared/api/container_exec.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // ContainerExecControl represents a message on the container exec "control" socket 4 | type ContainerExecControl struct { 5 | Command string `json:"command" yaml:"command"` 6 | Args map[string]string `json:"args" yaml:"args"` 7 | Signal int `json:"signal" yaml:"signal"` 8 | } 9 | 10 | // ContainerExecPost represents a LXD container exec request 11 | type ContainerExecPost struct { 12 | Command []string `json:"command" yaml:"command"` 13 | WaitForWS bool `json:"wait-for-websocket" yaml:"wait-for-websocket"` 14 | Interactive bool `json:"interactive" yaml:"interactive"` 15 | Environment map[string]string `json:"environment" yaml:"environment"` 16 | Width int `json:"width" yaml:"width"` 17 | Height int `json:"height" yaml:"height"` 18 | 19 | // API extension: container_exec_recording 20 | RecordOutput bool `json:"record-output" yaml:"record-output"` 21 | } 22 | -------------------------------------------------------------------------------- /shared/api/certificate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // CertificatesPost represents the fields of a new LXD certificate 4 | type CertificatesPost struct { 5 | CertificatePut `yaml:",inline"` 6 | 7 | Certificate string `json:"certificate" yaml:"certificate"` 8 | Password string `json:"password" yaml:"password"` 9 | } 10 | 11 | // CertificatePut represents the modifiable fields of a LXD certificate 12 | // 13 | // API extension: certificate_update 14 | type CertificatePut struct { 15 | Name string `json:"name" yaml:"name"` 16 | Type string `json:"type" yaml:"type"` 17 | } 18 | 19 | // Certificate represents a LXD certificate 20 | type Certificate struct { 21 | CertificatePut `yaml:",inline"` 22 | 23 | Certificate string `json:"certificate" yaml:"certificate"` 24 | Fingerprint string `json:"fingerprint" yaml:"fingerprint"` 25 | } 26 | 27 | // Writable converts a full Certificate struct into a CertificatePut struct (filters read-only fields) 28 | func (cert *Certificate) Writable() CertificatePut { 29 | return cert.CertificatePut 30 | } 31 | -------------------------------------------------------------------------------- /lxc/finger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/lxc/lxd" 5 | "github.com/lxc/lxd/shared/i18n" 6 | ) 7 | 8 | type fingerCmd struct{} 9 | 10 | func (c *fingerCmd) showByDefault() bool { 11 | return false 12 | } 13 | 14 | func (c *fingerCmd) usage() string { 15 | return i18n.G( 16 | `Usage: lxc finger [:] 17 | 18 | Check if the LXD server is alive.`) 19 | } 20 | 21 | func (c *fingerCmd) flags() {} 22 | 23 | func (c *fingerCmd) run(config *lxd.Config, args []string) error { 24 | if len(args) > 1 { 25 | return errArgs 26 | } 27 | 28 | var remote string 29 | if len(args) == 1 { 30 | remote = config.ParseRemote(args[0]) 31 | } else { 32 | remote = config.DefaultRemote 33 | } 34 | 35 | // New client may or may not need to connect to the remote host, but 36 | // client.ServerStatus will at least request the basic information from 37 | // the server. 38 | client, err := lxd.NewClient(config, remote) 39 | if err != nil { 40 | return err 41 | } 42 | _, err = client.ServerStatus() 43 | return err 44 | } 45 | -------------------------------------------------------------------------------- /lxd/daemon_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type daemonTestSuite struct { 10 | lxdTestSuite 11 | } 12 | 13 | func (suite *daemonTestSuite) Test_config_value_set_empty_removes_val() { 14 | var err error 15 | d := suite.d 16 | 17 | err = daemonConfig["core.trust_password"].Set(d, "foo") 18 | suite.Req.Nil(err) 19 | 20 | val := daemonConfig["core.trust_password"].Get() 21 | suite.Req.Equal(len(val), 192) 22 | 23 | valMap := daemonConfigRender() 24 | value, present := valMap["core.trust_password"] 25 | suite.Req.True(present) 26 | suite.Req.Equal(value, true) 27 | 28 | err = daemonConfig["core.trust_password"].Set(d, "") 29 | suite.Req.Nil(err) 30 | 31 | val = daemonConfig["core.trust_password"].Get() 32 | suite.Req.Equal(val, "") 33 | 34 | valMap = daemonConfigRender() 35 | _, present = valMap["core.trust_password"] 36 | suite.Req.False(present) 37 | } 38 | 39 | func TestDaemonTestSuite(t *testing.T) { 40 | suite.Run(t, new(daemonTestSuite)) 41 | } 42 | -------------------------------------------------------------------------------- /lxc/utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type utilsTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestUtilsTestSuite(t *testing.T) { 15 | suite.Run(t, new(utilsTestSuite)) 16 | } 17 | 18 | // StringList can be used to sort a list of strings. 19 | func (s *utilsTestSuite) Test_StringList() { 20 | data := [][]string{{"foo", "bar"}, {"baz", "bza"}} 21 | sort.Sort(StringList(data)) 22 | s.Equal([][]string{{"baz", "bza"}, {"foo", "bar"}}, data) 23 | } 24 | 25 | // The first different string is used in sorting. 26 | func (s *utilsTestSuite) Test_StringList_sort_by_column() { 27 | data := [][]string{{"foo", "baz"}, {"foo", "bar"}} 28 | sort.Sort(StringList(data)) 29 | s.Equal([][]string{{"foo", "bar"}, {"foo", "baz"}}, data) 30 | } 31 | 32 | // Empty strings are sorted last. 33 | func (s *utilsTestSuite) Test_StringList_empty_strings() { 34 | data := [][]string{{"", "bar"}, {"foo", "baz"}} 35 | sort.Sort(StringList(data)) 36 | s.Equal([][]string{{"foo", "baz"}, {"", "bar"}}, data) 37 | } 38 | -------------------------------------------------------------------------------- /test/suites/database_update.sh: -------------------------------------------------------------------------------- 1 | test_database_update(){ 2 | LXD_MIGRATE_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 3 | MIGRATE_DB=${LXD_MIGRATE_DIR}/lxd.db 4 | 5 | # Create the version 1 schema as the database 6 | sqlite3 "${MIGRATE_DB}" > /dev/null < deps/schema1.sql 7 | 8 | # Start an LXD demon in the tmp directory. This should start the updates. 9 | spawn_lxd "${LXD_MIGRATE_DIR}" true 10 | 11 | # Assert there are enough tables. 12 | expected_tables=23 13 | tables=$(sqlite3 "${MIGRATE_DB}" ".dump" | grep -c "CREATE TABLE") 14 | [ "${tables}" -eq "${expected_tables}" ] || { echo "FAIL: Wrong number of tables after database migration. Found: ${tables}, expected ${expected_tables}"; false; } 15 | 16 | # There should be 15 "ON DELETE CASCADE" occurrences 17 | expected_cascades=15 18 | cascades=$(sqlite3 "${MIGRATE_DB}" ".dump" | grep -c "ON DELETE CASCADE") 19 | [ "${cascades}" -eq "${expected_cascades}" ] || { echo "FAIL: Wrong number of ON DELETE CASCADE foreign keys. Found: ${cascades}, exected: ${expected_cascades}"; false; } 20 | 21 | kill_lxd "$LXD_MIGRATE_DIR" 22 | } 23 | -------------------------------------------------------------------------------- /test/suites/init_interactive.sh: -------------------------------------------------------------------------------- 1 | test_init_interactive() { 2 | # - lxd init 3 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 4 | chmod +x "${LXD_INIT_DIR}" 5 | spawn_lxd "${LXD_INIT_DIR}" false 6 | 7 | ( 8 | set -e 9 | # shellcheck disable=SC2034 10 | LXD_DIR=${LXD_INIT_DIR} 11 | 12 | # XXX We need to remove the eth0 device from the default profile, which 13 | # is typically attached by spawn_lxd. 14 | if lxc profile show default | grep -q eth0; then 15 | lxc network detach-profile lxdbr0 default eth0 16 | fi 17 | 18 | cat < 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void getucred(int sock, uint *uid, uint *gid, int *pid) { 19 | struct ucred peercred; 20 | socklen_t len; 21 | 22 | len = sizeof(struct ucred); 23 | 24 | if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &peercred, &len) != 0 || len != sizeof(peercred)) { 25 | fprintf(stderr, "getsockopt failed: %s\n", strerror(errno)); 26 | return; 27 | } 28 | 29 | *uid = peercred.uid; 30 | *gid = peercred.gid; 31 | *pid = peercred.pid; 32 | 33 | return; 34 | } 35 | */ 36 | import "C" 37 | 38 | func getUcred(fd int) (uint32, uint32, int32, error) { 39 | uid := C.uint(0) 40 | gid := C.uint(0) 41 | pid := C.int(-1) 42 | 43 | C.getucred(C.int(fd), &uid, &gid, &pid) 44 | 45 | if uid == 0 || gid == 0 || pid == -1 { 46 | return 0, 0, -1, errors.New("Failed to get the ucred") 47 | } 48 | 49 | return uint32(uid), uint32(gid), int32(pid), nil 50 | } 51 | -------------------------------------------------------------------------------- /shared/cert_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "encoding/pem" 5 | "testing" 6 | ) 7 | 8 | func TestGenerateMemCert(t *testing.T) { 9 | if testing.Short() { 10 | t.Skip("skipping cert generation in short mode") 11 | } 12 | cert, key, err := GenerateMemCert(false) 13 | if err != nil { 14 | t.Error(err) 15 | return 16 | } 17 | if cert == nil { 18 | t.Error("GenerateMemCert returned a nil cert") 19 | return 20 | } 21 | if key == nil { 22 | t.Error("GenerateMemCert returned a nil key") 23 | return 24 | } 25 | block, rest := pem.Decode(cert) 26 | if len(rest) != 0 { 27 | t.Errorf("GenerateMemCert returned a cert with trailing content: %q", string(rest)) 28 | } 29 | if block.Type != "CERTIFICATE" { 30 | t.Errorf("GenerateMemCert returned a cert with Type %q not \"CERTIFICATE\"", block.Type) 31 | } 32 | block, rest = pem.Decode(key) 33 | if len(rest) != 0 { 34 | t.Errorf("GenerateMemCert returned a key with trailing content: %q", string(rest)) 35 | } 36 | if block.Type != "RSA PRIVATE KEY" { 37 | t.Errorf("GenerateMemCert returned a cert with Type %q not \"RSA PRIVATE KEY\"", block.Type) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/suites/fdleak.sh: -------------------------------------------------------------------------------- 1 | test_fdleak() { 2 | LXD_FDLEAK_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 3 | chmod +x "${LXD_FDLEAK_DIR}" 4 | spawn_lxd "${LXD_FDLEAK_DIR}" true 5 | pid=$(cat "${LXD_FDLEAK_DIR}/lxd.pid") 6 | 7 | beforefds=$(/bin/ls "/proc/${pid}/fd" | wc -l) 8 | ( 9 | set -e 10 | # shellcheck disable=SC2034 11 | LXD_DIR=${LXD_FDLEAK_DIR} 12 | 13 | ensure_import_testimage 14 | 15 | for i in $(seq 5); do 16 | lxc init "testimage leaktest${i}" 17 | lxc info "leaktest${i}" 18 | lxc start "leaktest${i}" 19 | lxc exec "leaktest${i}" -- ps -ef 20 | lxc stop "leaktest${i}" --force 21 | lxc delete "leaktest${i}" 22 | done 23 | 24 | sleep 5 25 | 26 | exit 0 27 | ) 28 | afterfds=$(/bin/ls "/proc/${pid}/fd" | wc -l) 29 | leakedfds=$((afterfds - beforefds)) 30 | 31 | bad=0 32 | # shellcheck disable=SC2015 33 | [ ${leakedfds} -gt 5 ] && bad=1 || true 34 | if [ ${bad} -eq 1 ]; then 35 | echo "${leakedfds} FDS leaked" 36 | ls "/proc/${pid}/fd" -al 37 | netstat -anp 2>&1 | grep "${pid}/" 38 | false 39 | fi 40 | 41 | kill_lxd "${LXD_FDLEAK_DIR}" 42 | } 43 | -------------------------------------------------------------------------------- /shared/api/profile.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // ProfilesPost represents the fields of a new LXD profile 4 | type ProfilesPost struct { 5 | ProfilePut `yaml:",inline"` 6 | 7 | Name string `json:"name" yaml:"name"` 8 | } 9 | 10 | // ProfilePost represents the fields required to rename a LXD profile 11 | type ProfilePost struct { 12 | Name string `json:"name" yaml:"name"` 13 | } 14 | 15 | // ProfilePut represents the modifiable fields of a LXD profile 16 | type ProfilePut struct { 17 | Config map[string]string `json:"config" yaml:"config"` 18 | Description string `json:"description" yaml:"description"` 19 | Devices map[string]map[string]string `json:"devices" yaml:"devices"` 20 | } 21 | 22 | // Profile represents a LXD profile 23 | type Profile struct { 24 | ProfilePut `yaml:",inline"` 25 | 26 | Name string `json:"name" yaml:"name"` 27 | 28 | // API extension: profile_usedby 29 | UsedBy []string `json:"used_by" yaml:"used_by"` 30 | } 31 | 32 | // Writable converts a full Profile struct into a ProfilePut struct (filters read-only fields) 33 | func (profile *Profile) Writable() ProfilePut { 34 | return profile.ProfilePut 35 | } 36 | -------------------------------------------------------------------------------- /lxc/exec_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/mattn/go-colorable" 11 | 12 | "github.com/lxc/lxd" 13 | "github.com/lxc/lxd/shared/logger" 14 | ) 15 | 16 | // Windows doesn't process ANSI sequences natively, so we wrap 17 | // os.Stdout for improved user experience for Windows client 18 | type WrappedWriteCloser struct { 19 | io.Closer 20 | wrapper io.Writer 21 | } 22 | 23 | func (wwc *WrappedWriteCloser) Write(p []byte) (int, error) { 24 | return wwc.wrapper.Write(p) 25 | } 26 | 27 | func (c *execCmd) getStdout() io.WriteCloser { 28 | return &WrappedWriteCloser{os.Stdout, colorable.NewColorableStdout()} 29 | } 30 | 31 | func (c *execCmd) getTERM() (string, bool) { 32 | return "dumb", true 33 | } 34 | 35 | func (c *execCmd) controlSocketHandler(d *lxd.Client, control *websocket.Conn) { 36 | // TODO: figure out what the equivalent of signal.SIGWINCH is on 37 | // windows and use that; for now if you resize your terminal it just 38 | // won't work quite correctly. 39 | err := c.sendTermSize(control) 40 | if err != nil { 41 | logger.Debugf("error setting term size %s", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /doc/environment.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | The LXD client and daemon respect some environment variables to adapt to 3 | the user's environment and to turn some advanced features on and off. 4 | 5 | # Common 6 | Name | Description 7 | :--- | :---- 8 | LXD\_DIR | The LXD data directory 9 | PATH | List of paths to look into when resolving binaries 10 | http\_proxy | Proxy server URL for HTTP 11 | https\_proxy | Proxy server URL for HTTPs 12 | no\_proxy | List of domains that don't require the use of a proxy 13 | 14 | # Client environment variable 15 | Name | Description 16 | :--- | :---- 17 | EDITOR | What text editor to use 18 | VISUAL | What text editor to use (if EDITOR isn't set) 19 | 20 | # Server environment variable 21 | Name | Description 22 | :--- | :---- 23 | LXD\_SECURITY\_APPARMOR | If set to "false", forces AppArmor off 24 | LXD\_LXC\_TEMPLATE\_CONFIG | Path to the LXC template configuration directory 25 | -------------------------------------------------------------------------------- /client/lxd_operations.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gorilla/websocket" 7 | 8 | "github.com/lxc/lxd/shared/api" 9 | ) 10 | 11 | // GetOperation returns an Operation entry for the provided uuid 12 | func (r *ProtocolLXD) GetOperation(uuid string) (*api.Operation, string, error) { 13 | op := api.Operation{} 14 | 15 | // Fetch the raw value 16 | etag, err := r.queryStruct("GET", fmt.Sprintf("/operations/%s", uuid), nil, "", &op) 17 | if err != nil { 18 | return nil, "", err 19 | } 20 | 21 | return &op, etag, nil 22 | } 23 | 24 | // GetOperationWebsocket returns a websocket connection for the provided operation 25 | func (r *ProtocolLXD) GetOperationWebsocket(uuid string, secret string) (*websocket.Conn, error) { 26 | path := fmt.Sprintf("/operations/%s/websocket", uuid) 27 | if secret != "" { 28 | path = fmt.Sprintf("%s?secret=%s", path, secret) 29 | } 30 | 31 | return r.websocket(path) 32 | } 33 | 34 | // DeleteOperation deletes (cancels) a running operation 35 | func (r *ProtocolLXD) DeleteOperation(uuid string) error { 36 | // Send the request 37 | _, _, err := r.query("DELETE", fmt.Sprintf("/operations/%s", uuid), nil, "") 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /lxc/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | ) 7 | 8 | // Config holds settings to be used by a client or daemon 9 | type Config struct { 10 | // DefaultRemote holds the remote daemon name from the Remotes map 11 | // that the client should communicate with by default 12 | DefaultRemote string `yaml:"default-remote"` 13 | 14 | // Remotes defines a map of remote daemon names to the details for 15 | // communication with the named daemon 16 | Remotes map[string]Remote `yaml:"remotes"` 17 | 18 | // Command line aliases for `lxc` 19 | Aliases map[string]string `yaml:"aliases"` 20 | 21 | // Configuration directory 22 | ConfigDir string `yaml:"-"` 23 | 24 | // The UserAgent to pass for all queries 25 | UserAgent string `yaml:"-"` 26 | } 27 | 28 | // ConfigPath returns a joined path of the configuration directory and passed arguments 29 | func (c *Config) ConfigPath(paths ...string) string { 30 | path := []string{c.ConfigDir} 31 | path = append(path, paths...) 32 | 33 | return filepath.Join(path...) 34 | } 35 | 36 | // ServerCertPath returns the path for the remote's server certificate 37 | func (c *Config) ServerCertPath(remote string) string { 38 | return c.ConfigPath("servercerts", fmt.Sprintf("%s.crt", remote)) 39 | } 40 | -------------------------------------------------------------------------------- /lxd/db_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | _ "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | func dbConfigValuesGet(db *sql.DB) (map[string]string, error) { 10 | q := "SELECT key, value FROM config" 11 | rows, err := dbQuery(db, q) 12 | if err != nil { 13 | return map[string]string{}, err 14 | } 15 | defer rows.Close() 16 | 17 | results := map[string]string{} 18 | 19 | for rows.Next() { 20 | var key, value string 21 | rows.Scan(&key, &value) 22 | results[key] = value 23 | } 24 | 25 | return results, nil 26 | } 27 | 28 | func dbConfigValueSet(db *sql.DB, key string, value string) error { 29 | tx, err := dbBegin(db) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | _, err = tx.Exec("DELETE FROM config WHERE key=?", key) 35 | if err != nil { 36 | tx.Rollback() 37 | return err 38 | } 39 | 40 | if value != "" { 41 | str := `INSERT INTO config (key, value) VALUES (?, ?);` 42 | stmt, err := tx.Prepare(str) 43 | if err != nil { 44 | tx.Rollback() 45 | return err 46 | } 47 | defer stmt.Close() 48 | _, err = stmt.Exec(key, value) 49 | if err != nil { 50 | tx.Rollback() 51 | return err 52 | } 53 | } 54 | 55 | err = txCommit(tx) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /test/suites/profiling.sh: -------------------------------------------------------------------------------- 1 | test_cpu_profiling() { 2 | LXD3_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 3 | chmod +x "${LXD3_DIR}" 4 | spawn_lxd "${LXD3_DIR}" false --cpuprofile "${LXD3_DIR}/cpu.out" 5 | lxdpid=$(cat "${LXD3_DIR}/lxd.pid") 6 | kill -TERM "${lxdpid}" 7 | wait "${lxdpid}" || true 8 | export PPROF_TMPDIR="${TEST_DIR}/pprof" 9 | echo top5 | go tool pprof "$(which lxd)" "${LXD3_DIR}/cpu.out" 10 | echo "" 11 | 12 | # Cleanup following manual kill 13 | rm -f "${LXD3_DIR}/unix.socket" 14 | find "${LXD3_DIR}" -name shmounts -exec "umount" "-l" "{}" \; >/dev/null 2>&1 || true 15 | 16 | kill_lxd "${LXD3_DIR}" 17 | } 18 | 19 | test_mem_profiling() { 20 | LXD4_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 21 | chmod +x "${LXD4_DIR}" 22 | spawn_lxd "${LXD4_DIR}" false --memprofile "${LXD4_DIR}/mem" 23 | lxdpid=$(cat "${LXD4_DIR}/lxd.pid") 24 | 25 | if [ -e "${LXD4_DIR}/mem" ]; then 26 | false 27 | fi 28 | 29 | kill -USR1 "${lxdpid}" 30 | 31 | timeout=50 32 | while [ "${timeout}" != "0" ]; do 33 | [ -e "${LXD4_DIR}/mem" ] && break 34 | sleep 0.1 35 | timeout=$((timeout-1)) 36 | done 37 | 38 | export PPROF_TMPDIR="${TEST_DIR}/pprof" 39 | echo top5 | go tool pprof "$(which lxd)" "${LXD4_DIR}/mem" 40 | echo "" 41 | 42 | kill_lxd "${LXD4_DIR}" 43 | } 44 | -------------------------------------------------------------------------------- /lxd/migrate.proto: -------------------------------------------------------------------------------- 1 | package main; 2 | 3 | enum MigrationFSType { 4 | RSYNC = 0; 5 | BTRFS = 1; 6 | ZFS = 2; 7 | } 8 | 9 | enum CRIUType { 10 | CRIU_RSYNC = 0; 11 | PHAUL = 1; 12 | } 13 | 14 | message IDMapType { 15 | required bool isuid = 1; 16 | required bool isgid = 2; 17 | required int32 hostid = 3; 18 | required int32 nsid = 4; 19 | required int32 maprange = 5; 20 | } 21 | 22 | message Config { 23 | required string key = 1; 24 | required string value = 2; 25 | } 26 | 27 | message Device { 28 | required string name = 1; 29 | repeated Config config = 2; 30 | } 31 | 32 | message Snapshot { 33 | required string name = 1; 34 | repeated Config localConfig = 2; 35 | repeated string profiles = 3; 36 | required bool ephemeral = 4; 37 | repeated Device localDevices = 5; 38 | required int32 architecture = 6; 39 | required bool stateful = 7; 40 | } 41 | 42 | message MigrationHeader { 43 | required MigrationFSType fs = 1; 44 | optional CRIUType criu = 2; 45 | repeated IDMapType idmap = 3; 46 | repeated string snapshotNames = 4; 47 | repeated Snapshot snapshots = 5; 48 | } 49 | 50 | message MigrationControl { 51 | required bool success = 1; 52 | 53 | /* optional failure message if sending a failure */ 54 | optional string message = 2; 55 | } 56 | -------------------------------------------------------------------------------- /shared/network_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package shared 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/gorilla/websocket" 9 | 10 | "github.com/lxc/lxd/shared/logger" 11 | ) 12 | 13 | func WebsocketExecMirror(conn *websocket.Conn, w io.WriteCloser, r io.ReadCloser, exited chan bool, fd int) (chan bool, chan bool) { 14 | readDone := make(chan bool, 1) 15 | writeDone := make(chan bool, 1) 16 | 17 | go defaultWriter(conn, w, writeDone) 18 | 19 | go func(conn *websocket.Conn, r io.ReadCloser) { 20 | in := ExecReaderToChannel(r, -1, exited, fd) 21 | for { 22 | buf, ok := <-in 23 | if !ok { 24 | r.Close() 25 | logger.Debugf("sending write barrier") 26 | conn.WriteMessage(websocket.TextMessage, []byte{}) 27 | readDone <- true 28 | return 29 | } 30 | w, err := conn.NextWriter(websocket.BinaryMessage) 31 | if err != nil { 32 | logger.Debugf("Got error getting next writer %s", err) 33 | break 34 | } 35 | 36 | _, err = w.Write(buf) 37 | w.Close() 38 | if err != nil { 39 | logger.Debugf("Got err writing %s", err) 40 | break 41 | } 42 | } 43 | closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") 44 | conn.WriteMessage(websocket.CloseMessage, closeMsg) 45 | readDone <- true 46 | r.Close() 47 | }(conn, r) 48 | 49 | return readDone, writeDone 50 | } 51 | -------------------------------------------------------------------------------- /doc/daemon-behavior.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This specification covers some of the daemon's behavior, such as 4 | reaction to given signals, crashes, ... 5 | 6 | # Startup 7 | On every start, LXD checks that its directory structure exists. If it 8 | doesn't, it'll create the required directories, generate a keypair and 9 | initialize the database. 10 | 11 | Once the daemon is ready for work, LXD will scan the containers table 12 | for any container for which the stored power state differs from the 13 | current one. If a container's power state was recorded as running and the 14 | container isn't running, LXD will start it. 15 | 16 | # Signal handling 17 | ## SIGINT, SIGQUIT, SIGTERM 18 | For those signals, LXD assumes that it's being temporarily stopped and 19 | will be restarted at a later time to continue handling the containers. 20 | 21 | The containers will keep running and LXD will close all connections and 22 | exit cleanly. 23 | 24 | ## SIGPWR 25 | Indicates to LXD that the host is going down. 26 | 27 | LXD will attempt a clean shutdown of all the containers. After 30s, it 28 | will kill any remaining container. 29 | 30 | The container power\_state in the containers table is kept as it was so 31 | that LXD after the host is done rebooting can restore the containers as 32 | they were. 33 | 34 | ## SIGUSR1 35 | Write a memory profile dump to the file specified with \-\-memprofile. 36 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # How to run 2 | 3 | To run all tests, including the Go tests, run from repository root: 4 | 5 | sudo -E make check 6 | 7 | To run only the integration tests, run from the test directory: 8 | 9 | sudo -E ./main.sh 10 | 11 | # Environment variables 12 | 13 | Name | Default | Description 14 | :-- | :--- | :---------- 15 | LXD\_BACKEND | dir | What backend to test against (btrfs, dir, lvm, zfs, or random) 16 | LXD\_CONCURRENT | 0 | Run concurrency tests, very CPU intensive 17 | LXD\_DEBUG | 0 | Run lxd, lxc and the shell in debug mode (very verbose) 18 | LXD\_INSPECT | 0 | Don't teardown the test environment on failure 19 | LXD\_LOGS | "" | Path to a directory to copy all the LXD logs to 20 | LXD\_OFFLINE | 0 | Skip anything that requires network access 21 | LXD\_TEST\_IMAGE | "" (busybox test image) | Path to an image tarball to use instead of the default busybox image 22 | LXD\_TMPFS | 0 | Sets up a tmpfs for the whole testsuite to run on (fast but needs memory) 23 | -------------------------------------------------------------------------------- /lxd/main_callhook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/lxc/lxd/client" 9 | ) 10 | 11 | func cmdCallHook(args []string) error { 12 | // Parse the arguments 13 | if len(args) < 4 { 14 | return fmt.Errorf("Invalid arguments") 15 | } 16 | 17 | path := args[1] 18 | id := args[2] 19 | state := args[3] 20 | target := "" 21 | 22 | // Connect to LXD 23 | c, err := lxd.ConnectLXDUnix(fmt.Sprintf("%s/unix.socket", path), nil) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | // Prepare the request URL 29 | url := fmt.Sprintf("/internal/containers/%s/on%s", id, state) 30 | if state == "stop" { 31 | target = os.Getenv("LXC_TARGET") 32 | if target == "" { 33 | target = "unknown" 34 | } 35 | url = fmt.Sprintf("%s?target=%s", url, target) 36 | } 37 | 38 | // Setup the request 39 | hook := make(chan error, 1) 40 | go func() { 41 | _, _, err := c.RawQuery("GET", url, nil, "") 42 | if err != nil { 43 | hook <- err 44 | return 45 | } 46 | 47 | hook <- nil 48 | }() 49 | 50 | // Handle the timeout 51 | select { 52 | case err := <-hook: 53 | if err != nil { 54 | return err 55 | } 56 | break 57 | case <-time.After(30 * time.Second): 58 | return fmt.Errorf("Hook didn't finish within 30s") 59 | } 60 | 61 | if target == "reboot" { 62 | return fmt.Errorf("Reboot must be handled by LXD.") 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /shared/api/network.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // NetworksPost represents the fields of a new LXD network 4 | // 5 | // API extension: network 6 | type NetworksPost struct { 7 | NetworkPut `yaml:",inline"` 8 | 9 | Managed bool `json:"managed" yaml:"managed"` 10 | Name string `json:"name" yaml:"name"` 11 | Type string `json:"type" yaml:"type"` 12 | } 13 | 14 | // NetworkPost represents the fields required to rename a LXD network 15 | // 16 | // API extension: network 17 | type NetworkPost struct { 18 | Name string `json:"name" yaml:"name"` 19 | } 20 | 21 | // NetworkPut represents the modifiable fields of a LXD network 22 | // 23 | // API extension: network 24 | type NetworkPut struct { 25 | Config map[string]string `json:"config" yaml:"config"` 26 | 27 | // API extension: entity_description 28 | Description string `json:"description" yaml:"description"` 29 | } 30 | 31 | // Network represents a LXD network 32 | type Network struct { 33 | NetworkPut `yaml:",inline"` 34 | 35 | Name string `json:"name" yaml:"name"` 36 | Type string `json:"type" yaml:"type"` 37 | UsedBy []string `json:"used_by" yaml:"used_by"` 38 | 39 | // API extension: network 40 | Managed bool `json:"managed" yaml:"managed"` 41 | } 42 | 43 | // Writable converts a full Network struct into a NetworkPut struct (filters read-only fields) 44 | func (network *Network) Writable() NetworkPut { 45 | return network.NetworkPut 46 | } 47 | -------------------------------------------------------------------------------- /lxd/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | 11 | "github.com/lxc/lxd/shared" 12 | ) 13 | 14 | func WriteJSON(w http.ResponseWriter, body interface{}) error { 15 | var output io.Writer 16 | var captured *bytes.Buffer 17 | 18 | output = w 19 | if debug { 20 | captured = &bytes.Buffer{} 21 | output = io.MultiWriter(w, captured) 22 | } 23 | 24 | err := json.NewEncoder(output).Encode(body) 25 | 26 | if captured != nil { 27 | shared.DebugJson(captured) 28 | } 29 | 30 | return err 31 | } 32 | 33 | func etagHash(data interface{}) (string, error) { 34 | etag := sha256.New() 35 | err := json.NewEncoder(etag).Encode(data) 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | return fmt.Sprintf("%x", etag.Sum(nil)), nil 41 | } 42 | 43 | func etagCheck(r *http.Request, data interface{}) error { 44 | match := r.Header.Get("If-Match") 45 | if match == "" { 46 | return nil 47 | } 48 | 49 | hash, err := etagHash(data) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if hash != match { 55 | return fmt.Errorf("ETag doesn't match: %s vs %s", hash, match) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func loadModule(module string) error { 62 | if shared.PathExists(fmt.Sprintf("/sys/module/%s", module)) { 63 | return nil 64 | } 65 | 66 | _, err := shared.RunCommand("modprobe", module) 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /lxd/cgroup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | func getInitCgroupPath(controller string) string { 12 | f, err := os.Open("/proc/1/cgroup") 13 | if err != nil { 14 | return "/" 15 | } 16 | defer f.Close() 17 | 18 | scan := bufio.NewScanner(f) 19 | for scan.Scan() { 20 | line := scan.Text() 21 | 22 | fields := strings.Split(line, ":") 23 | if len(fields) != 3 { 24 | return "/" 25 | } 26 | 27 | if fields[2] != controller { 28 | continue 29 | } 30 | 31 | initPath := string(fields[3]) 32 | 33 | // ignore trailing /init.scope if it is there 34 | dir, file := path.Split(initPath) 35 | if file == "init.scope" { 36 | return dir 37 | } else { 38 | return initPath 39 | } 40 | } 41 | 42 | return "/" 43 | } 44 | 45 | func cGroupGet(controller, cgroup, file string) (string, error) { 46 | initPath := getInitCgroupPath(controller) 47 | path := path.Join("/sys/fs/cgroup", controller, initPath, cgroup, file) 48 | 49 | contents, err := ioutil.ReadFile(path) 50 | if err != nil { 51 | return "", err 52 | } 53 | return strings.Trim(string(contents), "\n"), nil 54 | } 55 | 56 | func cGroupSet(controller, cgroup, file string, value string) error { 57 | initPath := getInitCgroupPath(controller) 58 | path := path.Join("/sys/fs/cgroup", controller, initPath, cgroup, file) 59 | 60 | return ioutil.WriteFile(path, []byte(value), 0755) 61 | } 62 | -------------------------------------------------------------------------------- /shared/termios/termios_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package termios 4 | 5 | import ( 6 | "golang.org/x/crypto/ssh/terminal" 7 | ) 8 | 9 | // State contains the state of a terminal. 10 | type State terminal.State 11 | 12 | // IsTerminal returns true if the given file descriptor is a terminal. 13 | func IsTerminal(fd int) bool { 14 | return terminal.IsTerminal(fd) 15 | } 16 | 17 | // GetState returns the current state of a terminal which may be useful to restore the terminal after a signal. 18 | func GetState(fd int) (*State, error) { 19 | state, err := terminal.GetState(fd) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | currentState := State(*state) 25 | return ¤tState, nil 26 | } 27 | 28 | // GetSize returns the dimensions of the given terminal. 29 | func GetSize(fd int) (int, int, error) { 30 | return terminal.GetSize(fd) 31 | } 32 | 33 | // MakeRaw put the terminal connected to the given file descriptor into raw mode and returns the previous state of the terminal so that it can be restored. 34 | func MakeRaw(fd int) (*State, error) { 35 | state, err := terminal.MakeRaw(fd) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | oldState := State(*state) 41 | return &oldState, nil 42 | } 43 | 44 | // Restore restores the terminal connected to the given file descriptor to a previous state. 45 | func Restore(fd int, state *State) error { 46 | newState := terminal.State(*state) 47 | 48 | return terminal.Restore(fd, &newState) 49 | } 50 | -------------------------------------------------------------------------------- /lxc/restore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lxc/lxd" 7 | "github.com/lxc/lxd/shared" 8 | "github.com/lxc/lxd/shared/gnuflag" 9 | "github.com/lxc/lxd/shared/i18n" 10 | ) 11 | 12 | type restoreCmd struct { 13 | stateful bool 14 | } 15 | 16 | func (c *restoreCmd) showByDefault() bool { 17 | return true 18 | } 19 | 20 | func (c *restoreCmd) usage() string { 21 | return i18n.G( 22 | `Usage: lxc restore [:] [--stateful] 23 | 24 | Restore containers from snapshots. 25 | 26 | If --stateful is passed, then the running state will be restored too. 27 | 28 | *Examples* 29 | lxc snapshot u1 snap0 30 | Create the snapshot. 31 | 32 | lxc restore u1 snap0 33 | Restore the snapshot.`) 34 | } 35 | 36 | func (c *restoreCmd) flags() { 37 | gnuflag.BoolVar(&c.stateful, "stateful", false, i18n.G("Whether or not to restore the container's running state from snapshot (if available)")) 38 | } 39 | 40 | func (c *restoreCmd) run(config *lxd.Config, args []string) error { 41 | if len(args) < 2 { 42 | return errArgs 43 | } 44 | 45 | var snapname = args[1] 46 | 47 | remote, name := config.ParseRemoteAndContainer(args[0]) 48 | d, err := lxd.NewClient(config, remote) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if !shared.IsSnapshot(snapname) { 54 | snapname = fmt.Sprintf("%s/%s", name, snapname) 55 | } 56 | 57 | resp, err := d.RestoreSnapshot(name, snapname, c.stateful) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return d.WaitForSuccess(resp.Operation) 63 | } 64 | -------------------------------------------------------------------------------- /client/lxd_server.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "github.com/lxc/lxd/shared" 5 | "github.com/lxc/lxd/shared/api" 6 | ) 7 | 8 | // Server handling functions 9 | 10 | // GetServer returns the server status as a Server struct 11 | func (r *ProtocolLXD) GetServer() (*api.Server, string, error) { 12 | server := api.Server{} 13 | 14 | // Fetch the raw value 15 | etag, err := r.queryStruct("GET", "", nil, "", &server) 16 | if err != nil { 17 | return nil, "", err 18 | } 19 | 20 | // Fill in certificate fingerprint if not provided 21 | if server.Environment.CertificateFingerprint == "" && server.Environment.Certificate != "" { 22 | var err error 23 | server.Environment.CertificateFingerprint, err = shared.CertFingerprintStr(server.Environment.Certificate) 24 | if err != nil { 25 | return nil, "", err 26 | } 27 | } 28 | 29 | // Add the value to the cache 30 | r.server = &server 31 | 32 | return &server, etag, nil 33 | } 34 | 35 | // UpdateServer updates the server status to match the provided Server struct 36 | func (r *ProtocolLXD) UpdateServer(server api.ServerPut, ETag string) error { 37 | // Send the request 38 | _, _, err := r.query("PUT", "", server, ETag) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // HasExtension returns true if the server supports a given API extension 47 | func (r *ProtocolLXD) HasExtension(extension string) bool { 48 | for _, entry := range r.server.APIExtensions { 49 | if entry == extension { 50 | return true 51 | } 52 | } 53 | 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /lxc/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lxc/lxd" 7 | ) 8 | 9 | type aliasTestcase struct { 10 | input []string 11 | expected []string 12 | } 13 | 14 | func slicesEqual(a, b []string) bool { 15 | if a == nil && b == nil { 16 | return true 17 | } 18 | 19 | if a == nil || b == nil { 20 | return false 21 | } 22 | 23 | if len(a) != len(b) { 24 | return false 25 | } 26 | 27 | for i := range a { 28 | if a[i] != b[i] { 29 | return false 30 | } 31 | } 32 | 33 | return true 34 | } 35 | 36 | func TestExpandAliases(t *testing.T) { 37 | aliases := map[string]string{ 38 | "tester 12": "list", 39 | "foo": "list @ARGS@ -c n", 40 | } 41 | 42 | testcases := []aliasTestcase{ 43 | { 44 | input: []string{"lxc", "list"}, 45 | expected: []string{"lxc", "list"}, 46 | }, 47 | { 48 | input: []string{"lxc", "tester", "12"}, 49 | expected: []string{"lxc", "list", "--no-alias"}, 50 | }, 51 | { 52 | input: []string{"lxc", "foo", "asdf"}, 53 | expected: []string{"lxc", "list", "--no-alias", "asdf", "-c", "n"}, 54 | }, 55 | } 56 | 57 | conf := &lxd.Config{Aliases: aliases} 58 | 59 | for _, tc := range testcases { 60 | result, expanded := expandAlias(conf, tc.input) 61 | if !expanded { 62 | if !slicesEqual(tc.input, tc.expected) { 63 | t.Errorf("didn't expand when expected to: %s", tc.input) 64 | } 65 | continue 66 | } 67 | 68 | if !slicesEqual(result, tc.expected) { 69 | t.Errorf("%s didn't match %s", result, tc.expected) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lxd/main_forkmigrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "gopkg.in/lxc/go-lxc.v2" 9 | ) 10 | 11 | /* 12 | * Similar to forkstart, this is called when lxd is invoked as: 13 | * 14 | * lxd forkmigrate 15 | * 16 | * liblxc's restore() sets up the processes in such a way that the monitor ends 17 | * up being a child of the process that calls it, in our case lxd. However, we 18 | * really want the monitor to be daemonized, so we fork again. Additionally, we 19 | * want to fork for the same reasons we do forkstart (i.e. reduced memory 20 | * footprint when we fork tasks that will never free golang's memory, etc.) 21 | */ 22 | func cmdForkMigrate(args []string) error { 23 | if len(args) != 6 { 24 | return fmt.Errorf("Bad arguments %q", args) 25 | } 26 | 27 | name := args[1] 28 | lxcpath := args[2] 29 | configPath := args[3] 30 | imagesDir := args[4] 31 | preservesInodes, err := strconv.ParseBool(args[5]) 32 | 33 | c, err := lxc.NewContainer(name, lxcpath) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if err := c.LoadConfigFile(configPath); err != nil { 39 | return err 40 | } 41 | 42 | /* see https://github.com/golang/go/issues/13155, startContainer, and dc3a229 */ 43 | os.Stdin.Close() 44 | os.Stdout.Close() 45 | os.Stderr.Close() 46 | 47 | return c.Migrate(lxc.MIGRATE_RESTORE, lxc.MigrateOptions{ 48 | Directory: imagesDir, 49 | Verbose: true, 50 | PreservesInodes: preservesInodes, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /lxc/config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // LocalRemote is the default local remote (over the LXD unix socket) 4 | var LocalRemote = Remote{ 5 | Addr: "unix://", 6 | Static: true, 7 | Public: false, 8 | } 9 | 10 | // ImagesRemote is the community image server (over simplestreams) 11 | var ImagesRemote = Remote{ 12 | Addr: "https://images.linuxcontainers.org", 13 | Public: true, 14 | Protocol: "simplestreams", 15 | } 16 | 17 | // UbuntuRemote is the Ubuntu image server (over simplestreams) 18 | var UbuntuRemote = Remote{ 19 | Addr: "https://cloud-images.ubuntu.com/releases", 20 | Static: true, 21 | Public: true, 22 | Protocol: "simplestreams", 23 | } 24 | 25 | // UbuntuDailyRemote is the Ubuntu daily image server (over simplestreams) 26 | var UbuntuDailyRemote = Remote{ 27 | Addr: "https://cloud-images.ubuntu.com/daily", 28 | Static: true, 29 | Public: true, 30 | Protocol: "simplestreams", 31 | } 32 | 33 | // StaticRemotes is the list of remotes which can't be removed 34 | var StaticRemotes = map[string]Remote{ 35 | "local": LocalRemote, 36 | "ubuntu": UbuntuRemote, 37 | "ubuntu-daily": UbuntuDailyRemote, 38 | } 39 | 40 | // DefaultRemotes is the list of default remotes 41 | var DefaultRemotes = map[string]Remote{ 42 | "images": ImagesRemote, 43 | "local": LocalRemote, 44 | "ubuntu": UbuntuRemote, 45 | "ubuntu-daily": UbuntuDailyRemote, 46 | } 47 | 48 | // DefaultConfig is the default configuration 49 | var DefaultConfig = Config{ 50 | Remotes: DefaultRemotes, 51 | DefaultRemote: "local", 52 | } 53 | -------------------------------------------------------------------------------- /client_linux_test.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "syscall" 8 | "testing" 9 | ) 10 | 11 | func assertNoError(t *testing.T, err error, msg string) { 12 | if err != nil { 13 | t.Fatalf("Error: %s, action: %s", err, msg) 14 | } 15 | } 16 | 17 | func TestLocalLXDError(t *testing.T) { 18 | f, err := ioutil.TempFile("", "lxd-test.socket") 19 | assertNoError(t, err, "ioutil.TempFile to create fake socket file") 20 | defer os.RemoveAll(f.Name()) 21 | 22 | c := &Client{ 23 | Name: "test", 24 | Config: DefaultConfig, 25 | Remote: &RemoteConfig{ 26 | Addr: fmt.Sprintf("unix:%s", f.Name()), 27 | Static: true, 28 | Public: false, 29 | }, 30 | } 31 | runTest := func(exp error) { 32 | lxdErr := GetLocalLXDErr(connectViaUnix(c, c.Remote)) 33 | if lxdErr != exp { 34 | t.Fatalf("GetLocalLXDErr returned the wrong error, EXPECTED: %s, ACTUAL: %s", exp, lxdErr) 35 | } 36 | } 37 | 38 | // The fake socket file should mimic a socket with nobody listening. 39 | runTest(syscall.ECONNREFUSED) 40 | 41 | // Remove R/W permissions to mimic the user not having lxd group permissions. 42 | // Skip this test for root, as root ignores permissions and connect will fail 43 | // with ECONNREFUSED instead of EACCES. 44 | if os.Geteuid() != 0 { 45 | assertNoError(t, f.Chmod(0100), "f.Chmod on fake socket file") 46 | runTest(syscall.EACCES) 47 | } 48 | 49 | // Remove the fake socket to mimic LXD not being installed. 50 | assertNoError(t, os.RemoveAll(f.Name()), "osRemoveAll on fake socket file") 51 | runTest(syscall.ENOENT) 52 | } 53 | -------------------------------------------------------------------------------- /doc/backup.md: -------------------------------------------------------------------------------- 1 | # LXD Backup Strategies 2 | 3 | To backup a LXD instance different strategies are available. 4 | 5 | ## Full backup 6 | This requires that the whole `/var/lib/lxd` folder will be backuped up. 7 | Additionally, it is necessary to backup all storage pools as well. 8 | 9 | In order to restore the LXD instance the old `/var/lib/lxd` folder needs to be 10 | removed and replaced with the `/var/lib/lxd` snapshot. All storage pools 11 | need to be restored as well. 12 | 13 | ## Secondary LXD 14 | This requires a second LXD instance to be setup and reachable from the LXD 15 | instance that is to be backed up. Then, all containers can be copied to the 16 | secondary LXD instance for backup. 17 | 18 | ## Container backup and restore 19 | Additionally, LXD maintains a `backup.yaml` file in the containers storage 20 | volume. This file contains all necessary information to recover a given 21 | container. The tool `lxd import` is designed to process this file and to 22 | restore containers from it. 23 | This recovery mechanism is mostly meant for emergency recoveries and will try 24 | to re-create container and storage database entries from a backup of the 25 | storage pool configuration. This requires that the corresponding storage volume 26 | for the container exists and is accessible. For example, if the container's 27 | storage volume got unmounted the user is required to remount it manually. 28 | Note that if any existing database entry is found then `lxd import` will refuse 29 | to restore the container unless the `--force` flag is passed which will cause 30 | LXD to delete and replace any currently existing db entries. 31 | -------------------------------------------------------------------------------- /doc/architectures.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | LXD just like LXC can run on just about any architecture that's 3 | supported by the Linux kernel and by Go. 4 | 5 | Some objects in LXD are tied to an architecture, like the container, 6 | container snapshots and images. 7 | 8 | This document lists all the supported architectures, their unique 9 | identifier (used in the database), how they should be named and some 10 | notes. 11 | 12 | 13 | Please note that what LXD cares about is the kernel architecture, not 14 | the particular userspace flavor as determined by the toolchain. 15 | 16 | That means that LXD considers armv7 hard-float to be the same as armv7 17 | soft-float and refers to both as "armv7". If useful to the user, the 18 | exact userspace ABI may be set as an image and container property, 19 | allowing easy query. 20 | 21 | # Architectures 22 | 23 | ID | Name | Notes | Personalities 24 | :--- | :--- | :---- | :------------ 25 | 1 | i686 | 32bit Intel x86 | 26 | 2 | x86\_64 | 64bit Intel x86 | x86 27 | 3 | armv7l | 32bit ARMv7 little-endian | 28 | 4 | aarch64 | 64bit ARMv8 little-endian | armv7 (optional) 29 | 5 | ppc | 32bit PowerPC big-endian | 30 | 6 | ppc64 | 64bit PowerPC big-endian | powerpc 31 | 7 | ppc64le | 64bit PowerPC little-endian | 32 | 8 | s390x | 64bit ESA/390 big-endian | 33 | 34 | The architecture names above are typically aligned with the Linux kernel 35 | architecture names. 36 | -------------------------------------------------------------------------------- /shared/api/container_snapshot.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ContainerSnapshotsPost represents the fields available for a new LXD container snapshot 8 | type ContainerSnapshotsPost struct { 9 | Name string `json:"name" yaml:"name"` 10 | Stateful bool `json:"stateful" yaml:"stateful"` 11 | } 12 | 13 | // ContainerSnapshotPost represents the fields required to rename/move a LXD container snapshot 14 | type ContainerSnapshotPost struct { 15 | Name string `json:"name" yaml:"name"` 16 | Migration bool `json:"migration" yaml:"migration"` 17 | } 18 | 19 | // ContainerSnapshot represents a LXD conainer snapshot 20 | type ContainerSnapshot struct { 21 | Architecture string `json:"architecture" yaml:"architecture"` 22 | Config map[string]string `json:"config" yaml:"config"` 23 | CreationDate time.Time `json:"created_at" yaml:"created_at"` 24 | Devices map[string]map[string]string `json:"devices" yaml:"devices"` 25 | Ephemeral bool `json:"ephemeral" yaml:"ephemeral"` 26 | ExpandedConfig map[string]string `json:"expanded_config" yaml:"expanded_config"` 27 | ExpandedDevices map[string]map[string]string `json:"expanded_devices" yaml:"expanded_devices"` 28 | LastUsedDate time.Time `json:"last_used_at" yaml:"last_used_at"` 29 | Name string `json:"name" yaml:"name"` 30 | Profiles []string `json:"profiles" yaml:"profiles"` 31 | Stateful bool `json:"stateful" yaml:"stateful"` 32 | } 33 | -------------------------------------------------------------------------------- /test/suites/pki.sh: -------------------------------------------------------------------------------- 1 | test_pki() { 2 | if [ ! -d "/usr/share/easy-rsa/" ]; then 3 | echo "==> SKIP: The pki test requires easy-rsa to be installed" 4 | return 5 | fi 6 | 7 | # Setup the PKI 8 | cp -R /usr/share/easy-rsa "${TEST_DIR}/pki" 9 | ( 10 | set -e 11 | cd "${TEST_DIR}/pki" 12 | ls 13 | # shellcheck disable=SC1091 14 | . ./vars 15 | ./clean-all 16 | ./pkitool --initca 17 | ./pkitool --server 127.0.0.1 18 | ./pkitool lxd-client 19 | ) 20 | 21 | # Setup the daemon 22 | LXD5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 23 | chmod +x "${LXD5_DIR}" 24 | cat "${TEST_DIR}/pki/keys/127.0.0.1.crt" "${TEST_DIR}/pki/keys/ca.crt" > "${LXD5_DIR}/server.crt" 25 | cp "${TEST_DIR}/pki/keys/127.0.0.1.key" "${LXD5_DIR}/server.key" 26 | cp "${TEST_DIR}/pki/keys/ca.crt" "${LXD5_DIR}/server.ca" 27 | spawn_lxd "${LXD5_DIR}" true 28 | LXD5_ADDR=$(cat "${LXD5_DIR}/lxd.addr") 29 | 30 | # Setup the client 31 | LXC5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 32 | cp "${TEST_DIR}/pki/keys/lxd-client.crt" "${LXC5_DIR}/client.crt" 33 | cp "${TEST_DIR}/pki/keys/lxd-client.key" "${LXC5_DIR}/client.key" 34 | cp "${TEST_DIR}/pki/keys/ca.crt" "${LXC5_DIR}/client.ca" 35 | 36 | # Confirm that a valid client certificate works 37 | ( 38 | set -e 39 | export LXD_CONF=${LXC5_DIR} 40 | lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=foo 41 | lxc_remote info pki-lxd: 42 | ) 43 | 44 | # Confirm that a normal, non-PKI certificate doesn't 45 | ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=foo 46 | 47 | kill_lxd "${LXD5_DIR}" 48 | } 49 | -------------------------------------------------------------------------------- /lxd/main_forkstart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "syscall" 7 | 8 | "gopkg.in/lxc/go-lxc.v2" 9 | 10 | "github.com/lxc/lxd/shared" 11 | ) 12 | 13 | /* 14 | * This is called by lxd when called as "lxd forkstart " 15 | * 'forkstart' is used instead of just 'start' in the hopes that people 16 | * do not accidentally type 'lxd start' instead of 'lxc start' 17 | */ 18 | func cmdForkStart(args []string) error { 19 | if len(args) != 4 { 20 | return fmt.Errorf("Bad arguments: %q", args) 21 | } 22 | 23 | name := args[1] 24 | lxcpath := args[2] 25 | configPath := args[3] 26 | 27 | c, err := lxc.NewContainer(name, lxcpath) 28 | if err != nil { 29 | return fmt.Errorf("Error initializing container for start: %q", err) 30 | } 31 | 32 | err = c.LoadConfigFile(configPath) 33 | if err != nil { 34 | return fmt.Errorf("Error opening startup config file: %q", err) 35 | } 36 | 37 | /* due to https://github.com/golang/go/issues/13155 and the 38 | * CollectOutput call we make for the forkstart process, we need to 39 | * close our stdin/stdout/stderr here. Collecting some of the logs is 40 | * better than collecting no logs, though. 41 | */ 42 | os.Stdin.Close() 43 | os.Stderr.Close() 44 | os.Stdout.Close() 45 | 46 | // Redirect stdout and stderr to a log file 47 | logPath := shared.LogPath(name, "forkstart.log") 48 | if shared.PathExists(logPath) { 49 | os.Remove(logPath) 50 | } 51 | 52 | logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644) 53 | if err == nil { 54 | syscall.Dup3(int(logFile.Fd()), 1, 0) 55 | syscall.Dup3(int(logFile.Fd()), 2, 0) 56 | } 57 | 58 | return c.Start() 59 | } 60 | -------------------------------------------------------------------------------- /lxc/snapshot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lxc/lxd" 7 | "github.com/lxc/lxd/shared" 8 | "github.com/lxc/lxd/shared/gnuflag" 9 | "github.com/lxc/lxd/shared/i18n" 10 | ) 11 | 12 | type snapshotCmd struct { 13 | stateful bool 14 | } 15 | 16 | func (c *snapshotCmd) showByDefault() bool { 17 | return true 18 | } 19 | 20 | func (c *snapshotCmd) usage() string { 21 | return i18n.G( 22 | `Usage: lxc snapshot [:] [--stateful] 23 | 24 | Create container snapshots. 25 | 26 | When --stateful is used, LXD attempts to checkpoint the container's 27 | running state, including process memory state, TCP connections, ... 28 | 29 | *Examples* 30 | lxc snapshot u1 snap0 31 | Create a snapshot of "u1" called "snap0".`) 32 | } 33 | 34 | func (c *snapshotCmd) flags() { 35 | gnuflag.BoolVar(&c.stateful, "stateful", false, i18n.G("Whether or not to snapshot the container's running state")) 36 | } 37 | 38 | func (c *snapshotCmd) run(config *lxd.Config, args []string) error { 39 | if len(args) < 1 { 40 | return errArgs 41 | } 42 | 43 | var snapname string 44 | if len(args) < 2 { 45 | snapname = "" 46 | } else { 47 | snapname = args[1] 48 | } 49 | 50 | remote, name := config.ParseRemoteAndContainer(args[0]) 51 | d, err := lxd.NewClient(config, remote) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // we don't allow '/' in snapshot names 57 | if shared.IsSnapshot(snapname) { 58 | return fmt.Errorf(i18n.G("'/' not allowed in snapshot name")) 59 | } 60 | 61 | resp, err := d.Snapshot(name, snapname, c.stateful) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return d.WaitForSuccess(resp.Operation) 67 | } 68 | -------------------------------------------------------------------------------- /shared/api/status_code.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // StatusCode represents a valid LXD operation and container status 4 | type StatusCode int 5 | 6 | // LXD status codes 7 | const ( 8 | OperationCreated StatusCode = 100 9 | Started StatusCode = 101 10 | Stopped StatusCode = 102 11 | Running StatusCode = 103 12 | Cancelling StatusCode = 104 13 | Pending StatusCode = 105 14 | Starting StatusCode = 106 15 | Stopping StatusCode = 107 16 | Aborting StatusCode = 108 17 | Freezing StatusCode = 109 18 | Frozen StatusCode = 110 19 | Thawed StatusCode = 111 20 | Error StatusCode = 112 21 | 22 | Success StatusCode = 200 23 | 24 | Failure StatusCode = 400 25 | Cancelled StatusCode = 401 26 | ) 27 | 28 | // String returns a suitable string representation for the status code 29 | func (o StatusCode) String() string { 30 | return map[StatusCode]string{ 31 | OperationCreated: "Operation created", 32 | Started: "Started", 33 | Stopped: "Stopped", 34 | Running: "Running", 35 | Cancelling: "Cancelling", 36 | Pending: "Pending", 37 | Success: "Success", 38 | Failure: "Failure", 39 | Cancelled: "Cancelled", 40 | Starting: "Starting", 41 | Stopping: "Stopping", 42 | Aborting: "Aborting", 43 | Freezing: "Freezing", 44 | Frozen: "Frozen", 45 | Thawed: "Thawed", 46 | Error: "Error", 47 | }[o] 48 | } 49 | 50 | // IsFinal will return true if the status code indicates an end state 51 | func (o StatusCode) IsFinal() bool { 52 | return int(o) >= 200 53 | } 54 | -------------------------------------------------------------------------------- /lxc/manpage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | 9 | "github.com/lxc/lxd" 10 | "github.com/lxc/lxd/shared/i18n" 11 | ) 12 | 13 | type manpageCmd struct{} 14 | 15 | func (c *manpageCmd) showByDefault() bool { 16 | return false 17 | } 18 | 19 | func (c *manpageCmd) usage() string { 20 | return i18n.G( 21 | `Usage: lxc manpage 22 | 23 | Generate all the LXD manpages.`) 24 | } 25 | 26 | func (c *manpageCmd) flags() { 27 | } 28 | 29 | func (c *manpageCmd) run(_ *lxd.Config, args []string) error { 30 | if len(args) != 1 { 31 | return errArgs 32 | } 33 | 34 | _, err := exec.LookPath("help2man") 35 | if err != nil { 36 | return fmt.Errorf(i18n.G("Unable to find help2man.")) 37 | } 38 | 39 | help2man := func(command string, title string, path string) error { 40 | target, err := os.Create(path) 41 | if err != nil { 42 | return err 43 | } 44 | defer target.Close() 45 | 46 | cmd := exec.Command("help2man", command, "-n", title, "--no-info") 47 | cmd.Stdout = target 48 | 49 | return cmd.Run() 50 | } 51 | 52 | // Generate the main manpage 53 | err = help2man(fmt.Sprintf("%s --all", execName), "LXD - client", filepath.Join(args[0], fmt.Sprintf("lxc.1"))) 54 | if err != nil { 55 | return fmt.Errorf(i18n.G("Failed to generate 'lxc.1': %v"), err) 56 | } 57 | 58 | // Generate the pages for the subcommands 59 | for k, cmd := range commands { 60 | err := help2man(fmt.Sprintf("%s %s", execName, k), summaryLine(cmd.usage()), filepath.Join(args[0], fmt.Sprintf("lxc.%s.1", k))) 61 | if err != nil { 62 | return fmt.Errorf(i18n.G("Failed to generate 'lxc.%s.1': %v"), k, err) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /test/suites/security.sh: -------------------------------------------------------------------------------- 1 | test_security() { 2 | ensure_import_testimage 3 | ensure_has_localhost_remote "${LXD_ADDR}" 4 | 5 | # CVE-2016-1581 6 | if [ "$(storage_backend "$LXD_DIR")" = "zfs" ]; then 7 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 8 | chmod +x "${LXD_INIT_DIR}" 9 | spawn_lxd "${LXD_INIT_DIR}" false 10 | 11 | ZFS_POOL="lxdtest-$(basename "${LXD_DIR}")-init" 12 | LXD_DIR=${LXD_INIT_DIR} lxd init --storage-backend zfs --storage-create-loop 1 --storage-pool "${ZFS_POOL}" --auto 13 | 14 | PERM=$(stat -c %a "${LXD_INIT_DIR}/disks/${ZFS_POOL}.img") 15 | if [ "${PERM}" != "600" ]; then 16 | echo "Bad zfs.img permissions: ${PERM}" 17 | false 18 | fi 19 | 20 | kill_lxd "${LXD_INIT_DIR}" 21 | fi 22 | 23 | # CVE-2016-1582 24 | lxc launch testimage test-priv -c security.privileged=true 25 | 26 | PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-priv") 27 | if [ "${PERM}" != "700" ]; then 28 | echo "Bad container permissions: ${PERM}" 29 | false 30 | fi 31 | 32 | lxc config set test-priv security.privileged false 33 | lxc restart test-priv --force 34 | lxc config set test-priv security.privileged true 35 | lxc restart test-priv --force 36 | 37 | PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-priv") 38 | if [ "${PERM}" != "700" ]; then 39 | echo "Bad container permissions: ${PERM}" 40 | false 41 | fi 42 | 43 | lxc delete test-priv --force 44 | 45 | lxc launch testimage test-unpriv 46 | lxc config set test-unpriv security.privileged true 47 | lxc restart test-unpriv --force 48 | 49 | PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-unpriv") 50 | if [ "${PERM}" != "700" ]; then 51 | echo "Bad container permissions: ${PERM}" 52 | false 53 | fi 54 | 55 | lxc delete test-unpriv --force 56 | } 57 | -------------------------------------------------------------------------------- /shared/json.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/lxc/lxd/shared/logger" 9 | ) 10 | 11 | type Jmap map[string]interface{} 12 | 13 | func (m Jmap) GetString(key string) (string, error) { 14 | if val, ok := m[key]; !ok { 15 | return "", fmt.Errorf("Response was missing `%s`", key) 16 | } else if val, ok := val.(string); !ok { 17 | return "", fmt.Errorf("`%s` was not a string", key) 18 | } else { 19 | return val, nil 20 | } 21 | } 22 | 23 | func (m Jmap) GetMap(key string) (Jmap, error) { 24 | if val, ok := m[key]; !ok { 25 | return nil, fmt.Errorf("Response was missing `%s`", key) 26 | } else if val, ok := val.(map[string]interface{}); !ok { 27 | return nil, fmt.Errorf("`%s` was not a map, got %T", key, m[key]) 28 | } else { 29 | return val, nil 30 | } 31 | } 32 | 33 | func (m Jmap) GetInt(key string) (int, error) { 34 | if val, ok := m[key]; !ok { 35 | return -1, fmt.Errorf("Response was missing `%s`", key) 36 | } else if val, ok := val.(float64); !ok { 37 | return -1, fmt.Errorf("`%s` was not an int", key) 38 | } else { 39 | return int(val), nil 40 | } 41 | } 42 | 43 | func (m Jmap) GetBool(key string) (bool, error) { 44 | if val, ok := m[key]; !ok { 45 | return false, fmt.Errorf("Response was missing `%s`", key) 46 | } else if val, ok := val.(bool); !ok { 47 | return false, fmt.Errorf("`%s` was not an int", key) 48 | } else { 49 | return val, nil 50 | } 51 | } 52 | 53 | func DebugJson(r *bytes.Buffer) { 54 | pretty := &bytes.Buffer{} 55 | if err := json.Indent(pretty, r.Bytes(), "\t", "\t"); err != nil { 56 | logger.Debugf("error indenting json: %s", err) 57 | return 58 | } 59 | 60 | // Print the JSON without the last "\n" 61 | str := pretty.String() 62 | logger.Debugf("\n\t%s", str[0:len(str)-1]) 63 | } 64 | -------------------------------------------------------------------------------- /test/deps/schema1.sql: -------------------------------------------------------------------------------- 1 | -- Database schema version 1 as taken from febb96e8164fbd189698da77383c26ce68b9762a 2 | CREATE TABLE certificates ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 4 | fingerprint VARCHAR(255) NOT NULL, 5 | type INTEGER NOT NULL, 6 | name VARCHAR(255) NOT NULL, 7 | certificate TEXT NOT NULL, 8 | UNIQUE (fingerprint) 9 | ); 10 | CREATE TABLE containers ( 11 | id INTEGER primary key AUTOINCREMENT NOT NULL, 12 | name VARCHAR(255) NOT NULL, 13 | architecture INTEGER NOT NULL, 14 | type INTEGER NOT NULL, 15 | UNIQUE (name) 16 | ); 17 | CREATE TABLE containers_config ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 19 | container_id INTEGER NOT NULL, 20 | key VARCHAR(255) NOT NULL, 21 | value TEXT, 22 | FOREIGN KEY (container_id) REFERENCES containers (id), 23 | UNIQUE (container_id, key) 24 | ); 25 | CREATE TABLE images ( 26 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 27 | fingerprint VARCHAR(255) NOT NULL, 28 | filename VARCHAR(255) NOT NULL, 29 | size INTEGER NOT NULL, 30 | public INTEGER NOT NULL DEFAULT 0, 31 | architecture INTEGER NOT NULL, 32 | creation_date DATETIME, 33 | expiry_date DATETIME, 34 | upload_date DATETIME NOT NULL, 35 | UNIQUE (fingerprint) 36 | ); 37 | CREATE TABLE images_properties ( 38 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 39 | image_id INTEGER NOT NULL, 40 | type INTEGER NOT NULL, 41 | key VARCHAR(255) NOT NULL, 42 | value TEXT, 43 | FOREIGN KEY (image_id) REFERENCES images (id) 44 | ); 45 | CREATE TABLE schema ( 46 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 47 | version INTEGER NOT NULL, 48 | updated_at DATETIME NOT NULL, 49 | UNIQUE (version) 50 | ); 51 | INSERT INTO schema (version, updated_at) values (1, "now"); 52 | -------------------------------------------------------------------------------- /test/suites/fuidshift.sh: -------------------------------------------------------------------------------- 1 | test_common_fuidshift() { 2 | # test some bad arguments 3 | fail=0 4 | fuidshift > /dev/null 2>&1 && fail=1 5 | fuidshift -t > /dev/null 2>&1 && fail=1 6 | fuidshift /tmp -t b:0 > /dev/null 2>&1 && fail=1 7 | fuidshift /tmp -t x:0:0:0 > /dev/null 2>&1 && fail=1 8 | [ "${fail}" -ne 1 ] 9 | } 10 | 11 | test_nonroot_fuidshift() { 12 | test_common_fuidshift 13 | 14 | LXD_FUIDMAP_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 15 | 16 | u=$(id -u) 17 | g=$(id -g) 18 | u1=$((u+1)) 19 | g1=$((g+1)) 20 | 21 | touch "${LXD_FUIDMAP_DIR}/x1" 22 | fuidshift "${LXD_FUIDMAP_DIR}/x1" -t "u:${u}:100000:1" "g:${g}:100000:1" | tee /dev/stderr | grep "to 100000 100000" > /dev/null || fail=1 23 | if [ "${fail}" -eq 1 ]; then 24 | echo "==> Failed to shift own uid to container root" 25 | false 26 | fi 27 | fuidshift "${LXD_FUIDMAP_DIR}/x1" -t "u:${u1}:10000:1" "g:${g1}:100000:1" | tee /dev/stderr | grep "to -1 -1" > /dev/null || fail=1 28 | if [ "${fail}" -eq 1 ]; then 29 | echo "==> Wrongly shifted invalid uid to container root" 30 | false 31 | fi 32 | 33 | # unshift it 34 | chown 100000:100000 "${LXD_FUIDMAP_DIR}/x1" 35 | fuidshift "${LXD_FUIDMAP_DIR}/x1" -r -t "u:${u}:100000:1" "g:${g}:100000:1" | tee /dev/stderr | grep "to 0 0" > /dev/null || fail=1 36 | if [ "${fail}" -eq 1 ]; then 37 | echo "==> Failed to unshift container root back to own uid" 38 | false 39 | fi 40 | } 41 | 42 | test_root_fuidshift() { 43 | test_nonroot_fuidshift 44 | 45 | # Todo - test ranges 46 | } 47 | 48 | test_fuidshift() { 49 | if ! which fuidshift >/dev/null 2>&1; then 50 | echo "==> SKIP: No fuidshift binary could be found" 51 | return 52 | fi 53 | 54 | if [ "$(id -u)" -ne 0 ]; then 55 | test_nonroot_fuidshift 56 | else 57 | test_root_fuidshift 58 | fi 59 | } 60 | -------------------------------------------------------------------------------- /test/suites/image.sh: -------------------------------------------------------------------------------- 1 | test_image_expiry() { 2 | # shellcheck disable=2039 3 | local LXD2_DIR LXD2_ADDR 4 | LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 5 | chmod +x "${LXD2_DIR}" 6 | spawn_lxd "${LXD2_DIR}" true 7 | LXD2_ADDR=$(cat "${LXD2_DIR}/lxd.addr") 8 | 9 | ensure_import_testimage 10 | 11 | if ! lxc_remote remote list | grep -q l1; then 12 | # shellcheck disable=2153 13 | lxc_remote remote add l1 "${LXD_ADDR}" --accept-certificate --password foo 14 | fi 15 | if ! lxc_remote remote list | grep -q l2; then 16 | lxc_remote remote add l2 "${LXD2_ADDR}" --accept-certificate --password foo 17 | fi 18 | lxc_remote init l1:testimage l2:c1 19 | fp=$(lxc_remote image info testimage | awk -F: '/^Fingerprint/ { print $2 }' | awk '{ print $1 }') 20 | [ ! -z "${fp}" ] 21 | fpbrief=$(echo "${fp}" | cut -c 1-10) 22 | 23 | lxc_remote image list l2: | grep -q "${fpbrief}" 24 | 25 | lxc_remote remote set-default l2 26 | lxc_remote config set images.remote_cache_expiry 0 27 | lxc_remote remote set-default local 28 | 29 | ! lxc_remote image list l2: | grep -q "${fpbrief}" 30 | 31 | lxc_remote delete l2:c1 32 | 33 | # reset the default expiry 34 | lxc_remote remote set-default l2 35 | lxc_remote config set images.remote_cache_expiry 10 36 | lxc_remote remote set-default local 37 | 38 | lxc_remote remote remove l2 39 | kill_lxd "$LXD2_DIR" 40 | } 41 | 42 | test_image_list_all_aliases() { 43 | ensure_import_testimage 44 | # shellcheck disable=2039,2034,2155 45 | local sum=$(lxc image info testimage | grep ^Fingerprint | cut -d' ' -f2) 46 | lxc image alias create zzz "$sum" 47 | lxc image list | grep -vq zzz 48 | # both aliases are listed if the "aliases" column is included in output 49 | lxc image list -c L | grep -q testimage 50 | lxc image list -c L | grep -q zzz 51 | 52 | } 53 | -------------------------------------------------------------------------------- /test/suites/network.sh: -------------------------------------------------------------------------------- 1 | test_network() { 2 | ensure_import_testimage 3 | ensure_has_localhost_remote "${LXD_ADDR}" 4 | 5 | lxc init testimage nettest 6 | 7 | # Standard bridge with random subnet and a bunch of options 8 | lxc network create lxdt$$ 9 | lxc network set lxdt$$ dns.mode dynamic 10 | lxc network set lxdt$$ dns.domain blah 11 | lxc network set lxdt$$ ipv4.routing false 12 | lxc network set lxdt$$ ipv6.routing false 13 | lxc network set lxdt$$ ipv6.dhcp.stateful true 14 | lxc network delete lxdt$$ 15 | 16 | # edit network description 17 | lxc network create lxdt$$ 18 | lxc network show lxdt$$ | sed 's/^description:.*/description: foo/' | lxc network edit lxdt$$ 19 | lxc network show lxdt$$ | grep -q 'description: foo' 20 | lxc network delete lxdt$$ 21 | 22 | # Unconfigured bridge 23 | lxc network create lxdt$$ ipv4.address=none ipv6.address=none 24 | lxc network delete lxdt$$ 25 | 26 | # Configured bridge with static assignment 27 | lxc network create lxdt$$ dns.domain=test dns.mode=managed 28 | lxc network attach lxdt$$ nettest eth0 29 | v4_addr="$(lxc network get lxdt$$ ipv4.address | cut -d/ -f1)0" 30 | v6_addr="$(lxc network get lxdt$$ ipv4.address | cut -d/ -f1)00" 31 | lxc config device set nettest eth0 ipv4.address "${v4_addr}" 32 | lxc config device set nettest eth0 ipv6.address "${v6_addr}" 33 | grep -q "${v4_addr}.*nettest" "${LXD_DIR}/networks/lxdt$$/dnsmasq.hosts" 34 | grep -q "${v6_addr}.*nettest" "${LXD_DIR}/networks/lxdt$$/dnsmasq.hosts" 35 | lxc start nettest 36 | 37 | SUCCESS=0 38 | # shellcheck disable=SC2034 39 | for i in $(seq 10); do 40 | lxc info nettest | grep -q fd42 && SUCCESS=1 && break 41 | sleep 1 42 | done 43 | 44 | [ "${SUCCESS}" = "0" ] && (echo "Container static IP wasn't applied" && false) 45 | 46 | lxc delete nettest -f 47 | lxc network delete lxdt$$ 48 | } 49 | -------------------------------------------------------------------------------- /shared/ioprogress/tracker.go: -------------------------------------------------------------------------------- 1 | package ioprogress 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ProgressTracker provides the stream information needed for tracking 8 | type ProgressTracker struct { 9 | Length int64 10 | Handler func(int64, int64) 11 | 12 | percentage float64 13 | total int64 14 | start *time.Time 15 | last *time.Time 16 | } 17 | 18 | func (pt *ProgressTracker) update(n int) { 19 | // Skip the rest if no handler attached 20 | if pt.Handler == nil { 21 | return 22 | } 23 | 24 | // Initialize start time if needed 25 | if pt.start == nil { 26 | cur := time.Now() 27 | pt.start = &cur 28 | pt.last = pt.start 29 | } 30 | 31 | // Skip if no data to count 32 | if n <= 0 { 33 | return 34 | } 35 | 36 | // Update interval handling 37 | var percentage float64 38 | if pt.Length > 0 { 39 | // If running in relative mode, check that we increased by at least 1% 40 | percentage = float64(pt.total) / float64(pt.Length) * float64(100) 41 | if percentage-pt.percentage < 0.9 { 42 | return 43 | } 44 | } else { 45 | // If running in absolute mode, check that at least a second elapsed 46 | interval := time.Since(*pt.last).Seconds() 47 | if interval < 1 { 48 | return 49 | } 50 | } 51 | 52 | // Determine speed 53 | speedInt := int64(0) 54 | duration := time.Since(*pt.start).Seconds() 55 | if duration > 0 { 56 | speed := float64(pt.total) / duration 57 | speedInt = int64(speed) 58 | } 59 | 60 | // Determine progress 61 | progressInt := int64(0) 62 | if pt.Length > 0 { 63 | pt.percentage = percentage 64 | progressInt = int64(1 - (int(percentage) % 1) + int(percentage)) 65 | if progressInt > 100 { 66 | progressInt = 100 67 | } 68 | } else { 69 | progressInt = pt.total 70 | 71 | // Update timestamp 72 | cur := time.Now() 73 | pt.last = &cur 74 | } 75 | 76 | pt.Handler(progressInt, speedInt) 77 | } 78 | -------------------------------------------------------------------------------- /lxd/profiles_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | ) 7 | 8 | func Test_removing_a_profile_deletes_associated_configuration_entries(t *testing.T) { 9 | var db *sql.DB 10 | var err error 11 | 12 | d := &Daemon{} 13 | err = initializeDbObject(d, ":memory:") 14 | db = d.db 15 | 16 | // Insert a container and a related profile. Dont't forget that the profile 17 | // we insert is profile ID 2 (there is a default profile already). 18 | statements := ` 19 | INSERT INTO containers (name, architecture, type) VALUES ('thename', 1, 1); 20 | INSERT INTO profiles (name) VALUES ('theprofile'); 21 | INSERT INTO containers_profiles (container_id, profile_id) VALUES (1, 2); 22 | INSERT INTO profiles_devices (name, profile_id) VALUES ('somename', 2); 23 | INSERT INTO profiles_config (key, value, profile_id) VALUES ('thekey', 'thevalue', 2); 24 | INSERT INTO profiles_devices_config (profile_device_id, key, value) VALUES (1, 'something', 'boring');` 25 | 26 | _, err = db.Exec(statements) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | // Delete the profile we just created with dbProfileDelete 32 | err = dbProfileDelete(db, "theprofile") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | // Make sure there are 0 profiles_devices entries left. 38 | devices, err := dbDevices(d.db, "theprofile", true) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if len(devices) != 0 { 43 | t.Errorf("Deleting a profile didn't delete the related profiles_devices! There are %d left", len(devices)) 44 | } 45 | 46 | // Make sure there are 0 profiles_config entries left. 47 | config, err := dbProfileConfig(d.db, "theprofile") 48 | if err == nil { 49 | t.Fatal("found the profile!") 50 | } 51 | 52 | if len(config) != 0 { 53 | t.Errorf("Deleting a profile didn't delete the related profiles_config! There are %d left", len(config)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/deps/devlxd-client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * An example of how to use lxd's golang /dev/lxd client. This is intended to 5 | * be run from inside a container. 6 | */ 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "os" 15 | ) 16 | 17 | type devLxdDialer struct { 18 | Path string 19 | } 20 | 21 | func (d devLxdDialer) devLxdDial(network, path string) (net.Conn, error) { 22 | addr, err := net.ResolveUnixAddr("unix", d.Path) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | conn, err := net.DialUnix("unix", nil, addr) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return conn, err 33 | } 34 | 35 | var devLxdTransport = &http.Transport{ 36 | Dial: devLxdDialer{"/dev/lxd/sock"}.devLxdDial, 37 | } 38 | 39 | func main() { 40 | c := http.Client{Transport: devLxdTransport} 41 | raw, err := c.Get("http://meshuggah-rocks/") 42 | if err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | 47 | if raw.StatusCode != http.StatusOK { 48 | fmt.Println("http error", raw.StatusCode) 49 | result, err := ioutil.ReadAll(raw.Body) 50 | if err == nil { 51 | fmt.Println(string(result)) 52 | } 53 | os.Exit(1) 54 | } 55 | 56 | result := []string{} 57 | if err := json.NewDecoder(raw.Body).Decode(&result); err != nil { 58 | fmt.Println("err decoding response", err) 59 | os.Exit(1) 60 | } 61 | 62 | if result[0] != "/1.0" { 63 | fmt.Println("unknown response", result) 64 | os.Exit(1) 65 | } 66 | 67 | if len(os.Args) > 1 { 68 | raw, err := c.Get(fmt.Sprintf("http://meshuggah-rocks/1.0/config/%s", os.Args[1])) 69 | if err != nil { 70 | fmt.Println(err) 71 | os.Exit(1) 72 | } 73 | 74 | value, err := ioutil.ReadAll(raw.Body) 75 | if err != nil { 76 | fmt.Println(err) 77 | os.Exit(1) 78 | } 79 | 80 | fmt.Println(string(value)) 81 | } else { 82 | fmt.Println("/dev/lxd ok") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /fuidshift/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/lxc/lxd/shared" 8 | ) 9 | 10 | func help(me string, status int) { 11 | fmt.Printf("Usage: %s directory [-t] [-r] [ ...]\n", me) 12 | fmt.Printf(" -t implies test mode. No file ownerships will be changed.\n") 13 | fmt.Printf(" -r means reverse, that is shift the uids out of the container.\n") 14 | fmt.Printf("\n") 15 | fmt.Printf(" A range is [u|b|g]:.\n") 16 | fmt.Printf(" where u means shift uids, g means shift gids, b means shift both.\n") 17 | fmt.Printf(" For example: %s directory b:0:100000:65536 u:10000:1000:1\n", me) 18 | os.Exit(status) 19 | } 20 | 21 | func main() { 22 | if err := run(); err != nil { 23 | fmt.Printf("Error: %q\n", err) 24 | help(os.Args[0], 1) 25 | } 26 | } 27 | 28 | func run() error { 29 | if len(os.Args) < 3 { 30 | if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help" || os.Args[1] == "help") { 31 | help(os.Args[0], 0) 32 | } else { 33 | help(os.Args[0], 1) 34 | } 35 | } 36 | 37 | directory := os.Args[1] 38 | idmap := shared.IdmapSet{} 39 | testmode := false 40 | reverse := false 41 | 42 | for pos := 2; pos < len(os.Args); pos++ { 43 | 44 | switch os.Args[pos] { 45 | case "-r", "--reverse": 46 | reverse = true 47 | case "t", "-t", "--test", "test": 48 | testmode = true 49 | default: 50 | var err error 51 | idmap, err = idmap.Append(os.Args[pos]) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | } 57 | 58 | if idmap.Len() == 0 { 59 | fmt.Printf("No idmaps given\n") 60 | help(os.Args[0], 1) 61 | } 62 | 63 | if !testmode && os.Geteuid() != 0 { 64 | fmt.Printf("This must be run as root\n") 65 | os.Exit(1) 66 | } 67 | 68 | if reverse { 69 | return idmap.UidshiftFromContainer(directory, testmode) 70 | } 71 | return idmap.UidshiftIntoContainer(directory, testmode) 72 | } 73 | -------------------------------------------------------------------------------- /lxc/config/file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/lxc/lxd/shared" 12 | ) 13 | 14 | // LoadConfig reads the configuration from the config path; if the path does 15 | // not exist, it returns a default configuration. 16 | func LoadConfig(path string) (*Config, error) { 17 | // Open the config file 18 | content, err := ioutil.ReadFile(path) 19 | if err != nil { 20 | return nil, fmt.Errorf("Unable to read the configuration file: %v", err) 21 | } 22 | 23 | // Decode the yaml document 24 | c := Config{} 25 | err = yaml.Unmarshal(content, &c) 26 | if err != nil { 27 | return nil, fmt.Errorf("Unable to decode the configuration: %v", err) 28 | } 29 | 30 | // Set default values 31 | if c.Remotes == nil { 32 | c.Remotes = make(map[string]Remote) 33 | } 34 | c.ConfigDir = filepath.Dir(path) 35 | 36 | // Apply the static remotes 37 | for k, v := range StaticRemotes { 38 | c.Remotes[k] = v 39 | } 40 | 41 | return &c, nil 42 | } 43 | 44 | // SaveConfig writes the provided configuration to the config file. 45 | func (c *Config) SaveConfig(path string) error { 46 | // Create a new copy for the config file 47 | conf := Config{} 48 | err := shared.DeepCopy(c, &conf) 49 | if err != nil { 50 | return fmt.Errorf("Unable to copy the configuration: %v", err) 51 | } 52 | 53 | // Remove the static remotes 54 | for k := range StaticRemotes { 55 | delete(conf.Remotes, k) 56 | } 57 | 58 | // Create the config file (or truncate an existing one) 59 | f, err := os.Create(path) 60 | if err != nil { 61 | return fmt.Errorf("Unable to create the configuration file: %v", err) 62 | } 63 | defer f.Close() 64 | 65 | // Write the new config 66 | data, err := yaml.Marshal(c) 67 | _, err = f.Write(data) 68 | if err != nil { 69 | return fmt.Errorf("Unable to write the configuration: %v", err) 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /lxc/monitor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/lxc/lxd" 9 | "github.com/lxc/lxd/shared/gnuflag" 10 | "github.com/lxc/lxd/shared/i18n" 11 | ) 12 | 13 | type typeList []string 14 | 15 | func (f *typeList) String() string { 16 | return fmt.Sprint(*f) 17 | } 18 | 19 | func (f *typeList) Set(value string) error { 20 | if value == "" { 21 | return fmt.Errorf("Invalid type: %s", value) 22 | } 23 | 24 | if f == nil { 25 | *f = make(typeList, 1) 26 | } else { 27 | *f = append(*f, value) 28 | } 29 | return nil 30 | } 31 | 32 | type monitorCmd struct { 33 | typeArgs typeList 34 | } 35 | 36 | func (c *monitorCmd) showByDefault() bool { 37 | return false 38 | } 39 | 40 | func (c *monitorCmd) usage() string { 41 | return i18n.G( 42 | `Usage: lxc monitor [:] [--type=TYPE...] 43 | 44 | Monitor a local or remote LXD server. 45 | 46 | By default the monitor will listen to all message types. 47 | 48 | Message types to listen for can be specified with --type. 49 | 50 | *Examples* 51 | lxc monitor --type=logging 52 | Only show log message.`) 53 | } 54 | 55 | func (c *monitorCmd) flags() { 56 | gnuflag.Var(&c.typeArgs, "type", i18n.G("Event type to listen for")) 57 | } 58 | 59 | func (c *monitorCmd) run(config *lxd.Config, args []string) error { 60 | var remote string 61 | 62 | if len(args) > 1 { 63 | return errArgs 64 | } 65 | 66 | if len(args) == 0 { 67 | remote, _ = config.ParseRemoteAndContainer("") 68 | } else { 69 | remote, _ = config.ParseRemoteAndContainer(args[0]) 70 | } 71 | 72 | d, err := lxd.NewClient(config, remote) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | handler := func(message interface{}) { 78 | render, err := yaml.Marshal(&message) 79 | if err != nil { 80 | fmt.Printf("error: %s\n", err) 81 | return 82 | } 83 | 84 | fmt.Printf("%s\n\n", render) 85 | } 86 | 87 | return d.Monitor(c.typeArgs, handler, nil) 88 | } 89 | -------------------------------------------------------------------------------- /shared/util_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestFileCopy(t *testing.T) { 13 | helloWorld := []byte("hello world\n") 14 | source, err := ioutil.TempFile("", "") 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | defer os.Remove(source.Name()) 20 | 21 | if err := WriteAll(source, helloWorld); err != nil { 22 | source.Close() 23 | t.Error(err) 24 | return 25 | } 26 | source.Close() 27 | 28 | dest, err := ioutil.TempFile("", "") 29 | defer os.Remove(dest.Name()) 30 | if err != nil { 31 | t.Error(err) 32 | return 33 | } 34 | dest.Close() 35 | 36 | if err := FileCopy(source.Name(), dest.Name()); err != nil { 37 | t.Error(err) 38 | return 39 | } 40 | 41 | dest2, err := os.Open(dest.Name()) 42 | if err != nil { 43 | t.Error(err) 44 | return 45 | } 46 | 47 | content, err := ioutil.ReadAll(dest2) 48 | if err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | 53 | if string(content) != string(helloWorld) { 54 | t.Error("content mismatch: ", string(content), "!=", string(helloWorld)) 55 | return 56 | } 57 | } 58 | 59 | func TestReaderToChannel(t *testing.T) { 60 | buf := make([]byte, 1*1024*1024) 61 | rand.Read(buf) 62 | 63 | offset := 0 64 | finished := false 65 | 66 | ch := ReaderToChannel(bytes.NewBuffer(buf), -1) 67 | for { 68 | data, ok := <-ch 69 | if len(data) > 0 { 70 | for i := 0; i < len(data); i++ { 71 | if buf[offset+i] != data[i] { 72 | t.Error(fmt.Sprintf("byte %d didn't match", offset+i)) 73 | return 74 | } 75 | } 76 | 77 | offset += len(data) 78 | if offset > len(buf) { 79 | t.Error("read too much data") 80 | return 81 | } 82 | 83 | if offset == len(buf) { 84 | finished = true 85 | } 86 | } 87 | 88 | if !ok { 89 | if !finished { 90 | t.Error("connection closed too early") 91 | return 92 | } else { 93 | break 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/suites/image_prefer_cached.sh: -------------------------------------------------------------------------------- 1 | # In case a cached image matching the desired alias is present, that 2 | # one is preferred, even if the its remote has a more recent one. 3 | test_image_prefer_cached() { 4 | 5 | if lxc image alias list | grep -q "^| testimage\s*|.*$"; then 6 | lxc image delete testimage 7 | fi 8 | 9 | # shellcheck disable=2039 10 | local LXD2_DIR LXD2_ADDR 11 | LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 12 | chmod +x "${LXD2_DIR}" 13 | spawn_lxd "${LXD2_DIR}" true 14 | LXD2_ADDR=$(cat "${LXD2_DIR}/lxd.addr") 15 | 16 | (LXD_DIR=${LXD2_DIR} deps/import-busybox --alias testimage --public) 17 | fp1=$(LXD_DIR=${LXD2_DIR} lxc image info testimage | awk -F: '/^Fingerprint/ { print $2 }' | awk '{ print $1 }') 18 | 19 | lxc remote add l2 "${LXD2_ADDR}" --accept-certificate --password foo 20 | lxc init l2:testimage c1 21 | 22 | # Now the first image image is in the local store, since it was 23 | # downloaded to create c1. 24 | alias=$(lxc image info "${fp1}" | awk -F: '/^ Alias/ { print $2 }' | awk '{ print $1 }') 25 | [ "${alias}" = "testimage" ] 26 | 27 | # Delete the first image from the remote store and replace it with a 28 | # new one with a different fingerprint (passing "--template create" 29 | # will do that). 30 | (LXD_DIR=${LXD2_DIR} lxc image delete testimage) 31 | (LXD_DIR=${LXD2_DIR} deps/import-busybox --alias testimage --public --template create) 32 | fp2=$(LXD_DIR=${LXD2_DIR} lxc image info testimage | awk -F: '/^Fingerprint/ { print $2 }' | awk '{ print $1 }') 33 | [ "${fp1}" != "${fp2}" ] 34 | 35 | # At this point starting a new container from "testimage" should not 36 | # result in the new image being downloaded. 37 | lxc init l2:testimage c2 38 | if lxc image info "${fp2}"; then 39 | echo "The second image ${fp2} was downloaded and the cached one not used" 40 | return 1 41 | fi 42 | 43 | lxc delete c1 44 | lxc delete c2 45 | lxc remote remove l2 46 | lxc image delete "${fp1}" 47 | 48 | kill_lxd "$LXD2_DIR" 49 | } 50 | -------------------------------------------------------------------------------- /lxd/container_post.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/gorilla/mux" 10 | 11 | "github.com/lxc/lxd/shared" 12 | "github.com/lxc/lxd/shared/api" 13 | ) 14 | 15 | func containerPost(d *Daemon, r *http.Request) Response { 16 | name := mux.Vars(r)["name"] 17 | c, err := containerLoadByName(d, name) 18 | if err != nil { 19 | return SmartError(err) 20 | } 21 | 22 | body, err := ioutil.ReadAll(r.Body) 23 | if err != nil { 24 | return InternalError(err) 25 | } 26 | 27 | rdr1 := ioutil.NopCloser(bytes.NewBuffer(body)) 28 | rdr2 := ioutil.NopCloser(bytes.NewBuffer(body)) 29 | 30 | reqRaw := shared.Jmap{} 31 | err = json.NewDecoder(rdr1).Decode(&reqRaw) 32 | if err != nil { 33 | return BadRequest(err) 34 | } 35 | 36 | req := api.ContainerPost{} 37 | err = json.NewDecoder(rdr2).Decode(&req) 38 | if err != nil { 39 | return BadRequest(err) 40 | } 41 | 42 | // Check if stateful (backward compatibility) 43 | stateful := true 44 | _, err = reqRaw.GetBool("live") 45 | if err == nil { 46 | stateful = req.Live 47 | } 48 | 49 | if req.Migration { 50 | ws, err := NewMigrationSource(c, stateful, req.ContainerOnly) 51 | if err != nil { 52 | return InternalError(err) 53 | } 54 | 55 | resources := map[string][]string{} 56 | resources["containers"] = []string{name} 57 | 58 | op, err := operationCreate(operationClassWebsocket, resources, ws.Metadata(), ws.Do, nil, ws.Connect) 59 | if err != nil { 60 | return InternalError(err) 61 | } 62 | 63 | return OperationResponse(op) 64 | } 65 | 66 | // Check that the name isn't already in use 67 | id, _ := dbContainerId(d.db, req.Name) 68 | if id > 0 { 69 | return Conflict 70 | } 71 | 72 | run := func(*operation) error { 73 | return c.Rename(req.Name) 74 | } 75 | 76 | resources := map[string][]string{} 77 | resources["containers"] = []string{name} 78 | 79 | op, err := operationCreate(operationClassTask, resources, nil, run, nil, nil) 80 | if err != nil { 81 | return InternalError(err) 82 | } 83 | 84 | return OperationResponse(op) 85 | } 86 | -------------------------------------------------------------------------------- /lxd/containers_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/lxc/lxd/shared/api" 9 | "github.com/lxc/lxd/shared/logger" 10 | "github.com/lxc/lxd/shared/version" 11 | ) 12 | 13 | func containersGet(d *Daemon, r *http.Request) Response { 14 | for i := 0; i < 100; i++ { 15 | result, err := doContainersGet(d, d.isRecursionRequest(r)) 16 | if err == nil { 17 | return SyncResponse(true, result) 18 | } 19 | if !isDbLockedError(err) { 20 | logger.Debugf("DBERR: containersGet: error %q", err) 21 | return SmartError(err) 22 | } 23 | // 1 s may seem drastic, but we really don't want to thrash 24 | // perhaps we should use a random amount 25 | time.Sleep(100 * time.Millisecond) 26 | } 27 | 28 | logger.Debugf("DBERR: containersGet, db is locked") 29 | logger.Debugf(logger.GetStack()) 30 | return InternalError(fmt.Errorf("DB is locked")) 31 | } 32 | 33 | func doContainersGet(d *Daemon, recursion bool) (interface{}, error) { 34 | result, err := dbContainersList(d.db, cTypeRegular) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | resultString := []string{} 40 | resultList := []*api.Container{} 41 | if err != nil { 42 | return []string{}, err 43 | } 44 | 45 | for _, container := range result { 46 | if !recursion { 47 | url := fmt.Sprintf("/%s/containers/%s", version.APIVersion, container) 48 | resultString = append(resultString, url) 49 | } else { 50 | c, err := doContainerGet(d, container) 51 | if err != nil { 52 | c = &api.Container{ 53 | Name: container, 54 | Status: api.Error.String(), 55 | StatusCode: api.Error} 56 | } 57 | resultList = append(resultList, c) 58 | } 59 | } 60 | 61 | if !recursion { 62 | return resultString, nil 63 | } 64 | 65 | return resultList, nil 66 | } 67 | 68 | func doContainerGet(d *Daemon, cname string) (*api.Container, error) { 69 | c, err := containerLoadByName(d, cname) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | cts, _, err := c.Render() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return cts.(*api.Container), nil 80 | } 81 | -------------------------------------------------------------------------------- /test/deps/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFzjCCA7igAwIBAgIRAKnCQRdpkZ86oXYOd9hGrPgwCwYJKoZIhvcNAQELMB4x 3 | HDAaBgNVBAoTE2xpbnV4Y29udGFpbmVycy5vcmcwHhcNMTUwNzE1MDQ1NjQ0WhcN 4 | MjUwNzEyMDQ1NjQ0WjAeMRwwGgYDVQQKExNsaW51eGNvbnRhaW5lcnMub3JnMIIC 5 | IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyViJkCzoxa1NYilXqGJog6xz 6 | lSm4xt8KIzayc0JdB9VxEdIVdJqUzBAUtyCS4KZ9MbPmMEOX9NbBASL0tRK58/7K 7 | Scq99Kj4XbVMLU1P/y5aW0ymnF0OpKbG6unmgAI2k/duRlbYHvGRdhlswpKl0Yst 8 | l8i2kXOK0Rxcz90FewcEXGSnIYW21sz8YpBLfIZqOx6XEV36mOdi3MLrhUSAhXDw 9 | Pay33Y7NonCQUBtiO7BT938cqI14FJrWdKon1UnODtzONcVBLTWtoe7D41+mx7EE 10 | Taq5OPxBSe0DD6KQcPOZ7ZSJEhIqVKMvzLyiOJpyShmhm4OuGNoAG6jAuSij/9Kc 11 | aLU4IitcrvFOuAo8M9OpiY9ZCR7Gb/qaPAXPAxE7Ci3f9DDNKXtPXDjhj3YG01+h 12 | fNXMW3kCkMImn0A/+mZUMdCL87GWN2AN3Do5qaIc5XVEt1gp+LVqJeMoZ/lAeZWT 13 | IbzcnkneOzE25m+bjw3r3WlR26amhyrWNwjGzRkgfEpw336kniX/GmwaCNgdNk+g 14 | 5aIbVxIHO0DbgkDBtdljR3VOic4djW/LtUIYIQ2egnPPyRR3fcFI+x5EQdVQYUXf 15 | jpGIwovUDyG0Lkam2tpdeEXvLMZr8+Lhzu+H6vUFSj3cz6gcw/Xepw40FOkYdAI9 16 | LYB6nwpZLTVaOqZCJ2ECAwEAAaOCAQkwggEFMA4GA1UdDwEB/wQEAwIAoDATBgNV 17 | HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIHPBgNVHREEgccwgcSCCVVi 18 | dW50dVByb4IRMTAuMTY3LjE2MC4xODMvMjSCHzIwMDE6MTVjMDo2NzM1OmVlMDA6 19 | OmU6ZTMxMy8xMjiCKWZkNTc6Yzg3ZDpmMWVlOmVlMDA6MjFkOjdkZmY6ZmUwOToz 20 | NzUzLzY0gikyMDAxOjE1YzA6NjczNTplZTAwOjIxZDo3ZGZmOmZlMDk6Mzc1My82 21 | NIIbZmU4MDo6MjFkOjdkZmY6ZmUwOTozNzUzLzY0ghAxOTIuMTY4LjEyMi4xLzI0 22 | MAsGCSqGSIb3DQEBCwOCAgEAmcJUSBH7cLw3auEEV1KewtdqY1ARVB/pafAtbe9F 23 | 7ZKBbxUcS7cP3P1hRs5FH1bH44bIJKHxckctNUPqvC+MpXSryKinQ5KvGPNjGdlW 24 | 6EPlQr23btizC6hRdQ6RjEkCnQxhyTLmQ9n78nt47hjA96rFAhCUyfPdv9dI4Zux 25 | bBTJekhCx5taamQKoxr7tql4Y2TchVlwASZvOfar8I0GxBRFT8w9IjckOSLoT9/s 26 | OhlvXpeoxxFT7OHwqXEXdRUvw/8MGBo6JDnw+J/NGDBw3Z0goebG4FMT//xGSHia 27 | czl3A0M0flk4/45L7N6vctwSqi+NxVaJRKeiYPZyzOO9K/d+No+WVBPwKmyP8icQ 28 | b7FGTelPJOUolC6kmoyM+vyaNUoU4nz6lgOSHAtuqGNDWZWuX/gqzZw77hzDIgkN 29 | qisOHZWPVlG/iUh1JBkbglBaPeaa3zf0XwSdgwwf4v8Z+YtEiRqkuFgQY70eQKI/ 30 | CIkj1p0iW5IBEsEAGUGklz4ZwqJwH3lQIqDBzIgHe3EP4cXaYsx6oYhPSDdHLPv4 31 | HMZhl05DP75CEkEWRD0AIaL7SHdyuYUmCZ2zdrMI7TEDrAqcUuPbYpHcdJ2wnYmi 32 | 2G8XHJibfu4PCpIm1J8kPL8rqpdgW3moKR8Mp0HJQOH4tSBr1Ep7xNLP1wg6PIe+ 33 | p7U= 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /lxd/daemon_images_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/lxc/lxd/client" 8 | "github.com/lxc/lxd/shared/api" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type daemonImagesTestSuite struct { 13 | lxdTestSuite 14 | } 15 | 16 | // If the preferCached parameter of ImageDownload is set to true, and 17 | // an image with matching remote details is already present in the 18 | // database, and the auto-update settings is on, we won't download any 19 | // newer image even if available, and just use the cached one. 20 | func (suite *daemonImagesTestSuite) TestUseCachedImagesIfAvailable() { 21 | // Create an image with alias "test" and fingerprint "abcd". 22 | err := dbImageInsert(suite.d.db, "abcd", "foo.xz", 1, false, true, "amd64", time.Now(), time.Now(), map[string]string{}) 23 | suite.Req.Nil(err) 24 | id, _, err := dbImageGet(suite.d.db, "abcd", false, true) 25 | suite.Req.Nil(err) 26 | err = dbImageSourceInsert(suite.d.db, id, "img.srv", "simplestreams", "", "test") 27 | suite.Req.Nil(err) 28 | 29 | // Pretend we have already a non-expired entry for the remote 30 | // simplestream data, containing a more recent image for the 31 | // given alias. 32 | remote := lxd.ImageServer(&lxd.ProtocolSimpleStreams{}) 33 | alias := api.ImageAliasesEntry{Name: "test"} 34 | alias.Target = "other-more-recent-fingerprint" 35 | fingerprints := []string{"other-more-recent-fingerprint"} 36 | entry := &imageStreamCacheEntry{remote: remote, Aliases: []api.ImageAliasesEntry{alias}, Certificate: "", Fingerprints: fingerprints, expiry: time.Now().Add(time.Hour)} 37 | imageStreamCache["img.srv"] = entry 38 | defer delete(imageStreamCache, "img.srv") 39 | 40 | // Request an image with alias "test" and check that it's the 41 | // one we created above. 42 | op, err := operationCreate(operationClassTask, map[string][]string{}, nil, nil, nil, nil) 43 | suite.Req.Nil(err) 44 | image, err := suite.d.ImageDownload(op, "img.srv", "simplestreams", "", "", "test", false, false, "", true) 45 | suite.Req.Nil(err) 46 | suite.Req.Equal("abcd", image.Fingerprint) 47 | } 48 | 49 | func TestDaemonImagesTestSuite(t *testing.T) { 50 | suite.Run(t, new(daemonImagesTestSuite)) 51 | } 52 | -------------------------------------------------------------------------------- /lxd/main_activateifneeded.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/lxc/lxd/client" 8 | "github.com/lxc/lxd/shared" 9 | "github.com/lxc/lxd/shared/logger" 10 | ) 11 | 12 | func cmdActivateIfNeeded() error { 13 | // Only root should run this 14 | if os.Geteuid() != 0 { 15 | return fmt.Errorf("This must be run as root") 16 | } 17 | 18 | // Don't start a full daemon, we just need DB access 19 | d := &Daemon{ 20 | lxcpath: shared.VarPath("containers"), 21 | } 22 | 23 | if !shared.PathExists(shared.VarPath("lxd.db")) { 24 | logger.Debugf("No DB, so no need to start the daemon now.") 25 | return nil 26 | } 27 | 28 | err := initializeDbObject(d, shared.VarPath("lxd.db")) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | /* Load all config values from the database */ 34 | err = daemonConfigInit(d.db) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // Look for network socket 40 | value := daemonConfig["core.https_address"].Get() 41 | if value != "" { 42 | logger.Debugf("Daemon has core.https_address set, activating...") 43 | _, err := lxd.ConnectLXDUnix("", nil) 44 | return err 45 | } 46 | 47 | // Load the idmap for unprivileged containers 48 | d.IdmapSet, err = shared.DefaultIdmapSet() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Look for auto-started or previously started containers 54 | result, err := dbContainersList(d.db, cTypeRegular) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for _, name := range result { 60 | c, err := containerLoadByName(d, name) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | config := c.ExpandedConfig() 66 | lastState := config["volatile.last_state.power"] 67 | autoStart := config["boot.autostart"] 68 | 69 | if c.IsRunning() { 70 | logger.Debugf("Daemon has running containers, activating...") 71 | _, err := lxd.ConnectLXDUnix("", nil) 72 | return err 73 | } 74 | 75 | if lastState == "RUNNING" || lastState == "Running" || shared.IsTrue(autoStart) { 76 | logger.Debugf("Daemon has auto-started containers, activating...") 77 | _, err := lxd.ConnectLXDUnix("", nil) 78 | return err 79 | } 80 | } 81 | 82 | logger.Debugf("No need to start the daemon now.") 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /shared/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // ServerEnvironment represents the read-only environment fields of a LXD server 4 | type ServerEnvironment struct { 5 | Addresses []string `json:"addresses" yaml:"addresses"` 6 | Architectures []string `json:"architectures" yaml:"architectures"` 7 | Certificate string `json:"certificate" yaml:"certificate"` 8 | CertificateFingerprint string `json:"certificate_fingerprint" yaml:"certificate_fingerprint"` 9 | Driver string `json:"driver" yaml:"driver"` 10 | DriverVersion string `json:"driver_version" yaml:"driver_version"` 11 | Kernel string `json:"kernel" yaml:"kernel"` 12 | KernelArchitecture string `json:"kernel_architecture" yaml:"kernel_architecture"` 13 | KernelVersion string `json:"kernel_version" yaml:"kernel_version"` 14 | Server string `json:"server" yaml:"server"` 15 | ServerPid int `json:"server_pid" yaml:"server_pid"` 16 | ServerVersion string `json:"server_version" yaml:"server_version"` 17 | Storage string `json:"storage" yaml:"storage"` 18 | StorageVersion string `json:"storage_version" yaml:"storage_version"` 19 | } 20 | 21 | // ServerPut represents the modifiable fields of a LXD server configuration 22 | type ServerPut struct { 23 | Config map[string]interface{} `json:"config" yaml:"config"` 24 | } 25 | 26 | // ServerUntrusted represents a LXD server for an untrusted client 27 | type ServerUntrusted struct { 28 | APIExtensions []string `json:"api_extensions" yaml:"api_extensions"` 29 | APIStatus string `json:"api_status" yaml:"api_status"` 30 | APIVersion string `json:"api_version" yaml:"api_version"` 31 | Auth string `json:"auth" yaml:"auth"` 32 | Public bool `json:"public" yaml:"public"` 33 | } 34 | 35 | // Server represents a LXD server 36 | type Server struct { 37 | ServerPut `yaml:",inline"` 38 | ServerUntrusted `yaml:",inline"` 39 | 40 | Environment ServerEnvironment `json:"environment" yaml:"environment"` 41 | } 42 | 43 | // Writable converts a full Server struct into a ServerPut struct (filters read-only fields) 44 | func (srv *Server) Writable() ServerPut { 45 | return srv.ServerPut 46 | } 47 | -------------------------------------------------------------------------------- /shared/cmd/testing.go: -------------------------------------------------------------------------------- 1 | // In-memory streams, useful for testing cmd-related code. 2 | 3 | package cmd 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "strings" 9 | ) 10 | 11 | // MemoryStreams provide an in-memory version of the system 12 | // stdin/stdout/stderr streams. 13 | type MemoryStreams struct { 14 | in *strings.Reader 15 | out *bytes.Buffer 16 | err *bytes.Buffer 17 | } 18 | 19 | // NewMemoryStreams creates a new set of in-memory streams with the given 20 | // user input. 21 | func NewMemoryStreams(input string) *MemoryStreams { 22 | return &MemoryStreams{ 23 | in: strings.NewReader(input), 24 | out: new(bytes.Buffer), 25 | err: new(bytes.Buffer), 26 | } 27 | } 28 | 29 | // InputRead returns the current input string. 30 | func (s *MemoryStreams) InputRead() string { 31 | bytes, _ := ioutil.ReadAll(s.in) 32 | return string(bytes) 33 | } 34 | 35 | // Out returns the current content of the out stream. 36 | func (s *MemoryStreams) Out() string { 37 | return s.out.String() 38 | } 39 | 40 | // Err returns the current content of the err stream. 41 | func (s *MemoryStreams) Err() string { 42 | return s.err.String() 43 | } 44 | 45 | // InputReset replaces the data in the input stream. 46 | func (s *MemoryStreams) InputReset(input string) { 47 | // XXX This is what the stdlib strings.Reader.Reset() does, however 48 | // this method is not available in Go 1.6. 49 | *s.in = *strings.NewReader(input) 50 | } 51 | 52 | // InputAppend adds the given text to the current input. 53 | func (s *MemoryStreams) InputAppend(text string) { 54 | s.InputReset(s.InputRead() + text) 55 | } 56 | 57 | // InputAppendLine adds a single line to the input stream. 58 | func (s *MemoryStreams) InputAppendLine(line string) { 59 | s.InputAppend(line + "\n") 60 | } 61 | 62 | // InputAppendBoolAnswer adds a new "yes" or "no" line depending on the answer. 63 | func (s *MemoryStreams) InputAppendBoolAnswer(answer bool) { 64 | var line string 65 | if answer { 66 | line = "yes" 67 | } else { 68 | line = "no" 69 | } 70 | s.InputAppendLine(line) 71 | } 72 | 73 | // NewMemoryContext creates a new command Context using the given in-memory 74 | // streams. 75 | func NewMemoryContext(streams *MemoryStreams) *Context { 76 | return NewContext(streams.in, streams.out, streams.err) 77 | } 78 | -------------------------------------------------------------------------------- /lxd/networks_iptables.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared" 8 | ) 9 | 10 | func networkIptablesPrepend(protocol string, netName string, table string, chain string, rule ...string) error { 11 | cmd := "iptables" 12 | if protocol == "ipv6" { 13 | cmd = "ip6tables" 14 | } 15 | 16 | baseArgs := []string{"-w"} 17 | if table != "" { 18 | baseArgs = append(baseArgs, []string{"-t", table}...) 19 | } 20 | 21 | // Check for an existing entry 22 | args := append(baseArgs, []string{"-C", chain}...) 23 | args = append(args, rule...) 24 | args = append(args, "-m", "comment", "--comment", fmt.Sprintf("generated for LXD network %s", netName)) 25 | _, err := shared.RunCommand(cmd, args...) 26 | if err == nil { 27 | return nil 28 | } 29 | 30 | // Add the rule 31 | args = append(baseArgs, []string{"-I", chain}...) 32 | args = append(args, rule...) 33 | args = append(args, "-m", "comment", "--comment", fmt.Sprintf("generated for LXD network %s", netName)) 34 | 35 | _, err = shared.RunCommand(cmd, args...) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func networkIptablesClear(protocol string, netName string, table string) error { 44 | // Detect kernels that lack IPv6 support 45 | if !shared.PathExists("/proc/sys/net/ipv6") && protocol == "ipv6" { 46 | return nil 47 | } 48 | 49 | cmd := "iptables" 50 | if protocol == "ipv6" { 51 | cmd = "ip6tables" 52 | } 53 | 54 | baseArgs := []string{"-w"} 55 | if table != "" { 56 | baseArgs = append(baseArgs, []string{"-t", table}...) 57 | } 58 | 59 | // List the rules 60 | args := append(baseArgs, "-S") 61 | output, err := shared.RunCommand(cmd, args...) 62 | if err != nil { 63 | return fmt.Errorf("Failed to list %s rules for %s (table %s)", protocol, netName, table) 64 | } 65 | 66 | for _, line := range strings.Split(output, "\n") { 67 | if !strings.Contains(line, fmt.Sprintf("generated for LXD network %s", netName)) { 68 | continue 69 | } 70 | 71 | // Remove the entry 72 | fields := strings.Fields(line) 73 | fields[0] = "-D" 74 | 75 | args = append(baseArgs, fields...) 76 | _, err = shared.RunCommand("sh", "-c", fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))) 77 | if err != nil { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /shared/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // ResponseRaw represents a LXD operation in its original form 8 | type ResponseRaw struct { 9 | Response `yaml:",inline"` 10 | 11 | Metadata interface{} `json:"metadata" yaml:"metadata"` 12 | } 13 | 14 | // Response represents a LXD operation 15 | type Response struct { 16 | Type ResponseType `json:"type" yaml:"type"` 17 | 18 | // Valid only for Sync responses 19 | Status string `json:"status" yaml:"status"` 20 | StatusCode int `json:"status_code" yaml:"status_code"` 21 | 22 | // Valid only for Async responses 23 | Operation string `json:"operation" yaml:"operation"` 24 | 25 | // Valid only for Error responses 26 | Code int `json:"error_code" yaml:"error_code"` 27 | Error string `json:"error" yaml:"error"` 28 | 29 | // Valid for Sync and Error responses 30 | Metadata json.RawMessage `json:"metadata" yaml:"metadata"` 31 | } 32 | 33 | // MetadataAsMap parses the Response metadata into a map 34 | func (r *Response) MetadataAsMap() (map[string]interface{}, error) { 35 | ret := map[string]interface{}{} 36 | err := r.MetadataAsStruct(&ret) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return ret, nil 42 | } 43 | 44 | // MetadataAsOperation turns the Response metadata into an Operation 45 | func (r *Response) MetadataAsOperation() (*Operation, error) { 46 | op := Operation{} 47 | err := r.MetadataAsStruct(&op) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &op, nil 53 | } 54 | 55 | // MetadataAsStringSlice parses the Response metadata into a slice of string 56 | func (r *Response) MetadataAsStringSlice() ([]string, error) { 57 | sl := []string{} 58 | err := r.MetadataAsStruct(&sl) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return sl, nil 64 | } 65 | 66 | // MetadataAsStruct parses the Response metadata into a provided struct 67 | func (r *Response) MetadataAsStruct(target interface{}) error { 68 | if err := json.Unmarshal(r.Metadata, &target); err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // ResponseType represents a valid LXD response type 76 | type ResponseType string 77 | 78 | // LXD response types 79 | const ( 80 | SyncResponse ResponseType = "sync" 81 | AsyncResponse ResponseType = "async" 82 | ErrorResponse ResponseType = "error" 83 | ) 84 | -------------------------------------------------------------------------------- /lxc/help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | 8 | "github.com/lxc/lxd" 9 | "github.com/lxc/lxd/shared/gnuflag" 10 | "github.com/lxc/lxd/shared/i18n" 11 | ) 12 | 13 | type helpCmd struct { 14 | showAll bool 15 | } 16 | 17 | func (c *helpCmd) showByDefault() bool { 18 | return true 19 | } 20 | 21 | func (c *helpCmd) usage() string { 22 | return i18n.G( 23 | `Usage: lxc help [--all] 24 | 25 | Help page for the LXD client.`) 26 | } 27 | 28 | func (c *helpCmd) flags() { 29 | gnuflag.BoolVar(&c.showAll, "all", false, i18n.G("Show all commands (not just interesting ones)")) 30 | } 31 | 32 | func (c *helpCmd) run(config *lxd.Config, args []string) error { 33 | if len(args) > 0 { 34 | for _, name := range args { 35 | cmd, ok := commands[name] 36 | if !ok { 37 | fmt.Fprintf(os.Stderr, i18n.G("error: unknown command: %s")+"\n", name) 38 | } else { 39 | fmt.Fprintf(os.Stdout, cmd.usage()+"\n") 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | fmt.Println(i18n.G("Usage: lxc [options]")) 46 | fmt.Println() 47 | fmt.Println(i18n.G(`This is the LXD command line client. 48 | 49 | All of LXD's features can be driven through the various commands below. 50 | For help with any of those, simply call them with --help.`)) 51 | fmt.Println() 52 | 53 | fmt.Println(i18n.G("Commands:")) 54 | var names []string 55 | for name := range commands { 56 | names = append(names, name) 57 | } 58 | sort.Strings(names) 59 | for _, name := range names { 60 | if name == "help" { 61 | continue 62 | } 63 | 64 | cmd := commands[name] 65 | if c.showAll || cmd.showByDefault() { 66 | fmt.Printf(" %-16s %s\n", name, summaryLine(cmd.usage())) 67 | } 68 | } 69 | 70 | fmt.Println() 71 | fmt.Println(i18n.G("Options:")) 72 | fmt.Println(" --all " + i18n.G("Print less common commands")) 73 | fmt.Println(" --debug " + i18n.G("Print debug information")) 74 | fmt.Println(" --verbose " + i18n.G("Print verbose information")) 75 | fmt.Println(" --version " + i18n.G("Show client version")) 76 | fmt.Println() 77 | fmt.Println(i18n.G("Environment:")) 78 | fmt.Println(" LXD_CONF " + i18n.G("Path to an alternate client configuration directory")) 79 | fmt.Println(" LXD_DIR " + i18n.G("Path to an alternate server directory")) 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /shared/termios/termios.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package termios 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | 9 | "github.com/lxc/lxd/shared" 10 | ) 11 | 12 | // #include 13 | import "C" 14 | 15 | // State contains the state of a terminal. 16 | type State struct { 17 | Termios syscall.Termios 18 | } 19 | 20 | // IsTerminal returns true if the given file descriptor is a terminal. 21 | func IsTerminal(fd int) bool { 22 | _, err := GetState(fd) 23 | return err == nil 24 | } 25 | 26 | // GetState returns the current state of a terminal which may be useful to restore the terminal after a signal. 27 | func GetState(fd int) (*State, error) { 28 | termios := syscall.Termios{} 29 | 30 | ret, err := C.tcgetattr(C.int(fd), (*C.struct_termios)(unsafe.Pointer(&termios))) 31 | if ret != 0 { 32 | return nil, err.(syscall.Errno) 33 | } 34 | 35 | state := State{} 36 | state.Termios = termios 37 | 38 | return &state, nil 39 | } 40 | 41 | // GetSize returns the dimensions of the given terminal. 42 | func GetSize(fd int) (int, int, error) { 43 | var dimensions [4]uint16 44 | 45 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { 46 | return -1, -1, err 47 | } 48 | 49 | return int(dimensions[1]), int(dimensions[0]), nil 50 | } 51 | 52 | // MakeRaw put the terminal connected to the given file descriptor into raw mode and returns the previous state of the terminal so that it can be restored. 53 | func MakeRaw(fd int) (*State, error) { 54 | var err error 55 | var oldState, newState *State 56 | 57 | oldState, err = GetState(fd) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | err = shared.DeepCopy(&oldState, &newState) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&newState.Termios))) 68 | 69 | err = Restore(fd, newState) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return oldState, nil 75 | } 76 | 77 | // Restore restores the terminal connected to the given file descriptor to a previous state. 78 | func Restore(fd int, state *State) error { 79 | ret, err := C.tcsetattr(C.int(fd), C.TCSANOW, (*C.struct_termios)(unsafe.Pointer(&state.Termios))) 80 | if ret != 0 { 81 | return err.(syscall.Errno) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /lxc/move.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/lxc/lxd" 5 | "github.com/lxc/lxd/shared/i18n" 6 | 7 | "github.com/lxc/lxd/shared/gnuflag" 8 | ) 9 | 10 | type moveCmd struct { 11 | containerOnly bool 12 | } 13 | 14 | func (c *moveCmd) showByDefault() bool { 15 | return true 16 | } 17 | 18 | func (c *moveCmd) usage() string { 19 | return i18n.G( 20 | `Usage: lxc move [:][/] [:][[/]] [--container-only] 21 | 22 | Move containers within or in between LXD instances. 23 | 24 | lxc move [:] [:][] [--container-only] 25 | Move a container between two hosts, renaming it if destination name differs. 26 | 27 | lxc move [--container-only] 28 | Rename a local container. 29 | 30 | lxc move / / 31 | Rename a snapshot.`) 32 | } 33 | 34 | func (c *moveCmd) flags() { 35 | gnuflag.BoolVar(&c.containerOnly, "container-only", false, i18n.G("Move the container without its snapshots")) 36 | } 37 | 38 | func (c *moveCmd) run(config *lxd.Config, args []string) error { 39 | if len(args) != 2 { 40 | return errArgs 41 | } 42 | 43 | sourceRemote, sourceName := config.ParseRemoteAndContainer(args[0]) 44 | destRemote, destName := config.ParseRemoteAndContainer(args[1]) 45 | 46 | // As an optimization, if the source an destination are the same, do 47 | // this via a simple rename. This only works for containers that aren't 48 | // running, containers that are running should be live migrated (of 49 | // course, this changing of hostname isn't supported right now, so this 50 | // simply won't work). 51 | if sourceRemote == destRemote { 52 | source, err := lxd.NewClient(config, sourceRemote) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | rename, err := source.Rename(sourceName, destName) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return source.WaitForSuccess(rename.Operation) 63 | } 64 | 65 | cpy := copyCmd{} 66 | 67 | // A move is just a copy followed by a delete; however, we want to 68 | // keep the volatile entries around since we are moving the container. 69 | err := cpy.copyContainer(config, args[0], args[1], true, -1, true, c.containerOnly) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return commands["delete"].run(config, args[:1]) 75 | } 76 | -------------------------------------------------------------------------------- /doc/debugging.md: -------------------------------------------------------------------------------- 1 | Here are different ways to help troubleshooting `lxc` and `lxd` code. 2 | 3 | #### lxc --debug 4 | 5 | Adding `--debug` flag to any client command will give extra information 6 | about internals. If there is no useful info, it can be added with the 7 | logging call: 8 | 9 | logger.Debugf("Hello: %s", "Debug") 10 | 11 | #### lxc monitor 12 | 13 | This command will monitor messages as they appear on remote server. 14 | 15 | #### lxd --debug 16 | 17 | Shutting down `lxd` server and running it in foreground with `--debug` 18 | flag will bring a lot of (hopefully) useful info: 19 | 20 | systemctl stop lxd lxd.socket 21 | lxd --debug --group lxd 22 | 23 | `--group lxd` is needed to grant access to unprivileged users in this 24 | group. 25 | 26 | 27 | ### REST API through local socket 28 | 29 | On server side the most easy way is to communicate with LXD through 30 | local socket. This command accesses `GET /1.0` and formats JSON into 31 | human readable form using [jq](https://stedolan.github.io/jq/tutorial/) 32 | utility: 33 | 34 | curl --unix-socket /var/lib/lxd/unix.socket lxd/1.0 | jq . 35 | 36 | See [rest-api.md](rest-api.md) for available API. 37 | 38 | 39 | ### REST API through HTTPS 40 | 41 | [HTTPS connection to LXD](lxd-ssl-authentication.md) requires valid 42 | client certificate, generated in `~/.config/lxc/client.crt` on 43 | first `lxc remote add`. This certificate should be passed to 44 | connection tools for authentication and encryption. 45 | 46 | Examining certificate. In case you are curious: 47 | 48 | openssl x509 -in client.crt -purpose 49 | 50 | Among the lines you should see: 51 | 52 | Certificate purposes: 53 | SSL client : Yes 54 | 55 | #### with command line tools 56 | 57 | wget --no-check-certificate https://127.0.0.1:8443/1.0 --certificate=$HOME/.config/lxc/client.crt --private-key=$HOME/.config/lxc/client.key -O - -q 58 | 59 | #### with browser 60 | 61 | Some browser plugins provide convenient interface to create, modify 62 | and replay web requests. To authenticate againsg LXD server, convert 63 | `lxc` client certificate into importable format and import it into 64 | browser. 65 | 66 | For example this produces `client.pfx` in Windows-compatible format: 67 | 68 | openssl pkcs12 -clcerts -inkey client.key -in client.crt -export -out client.pfx 69 | 70 | After that, opening https://127.0.0.1:8443/1.0 should work as expected. 71 | -------------------------------------------------------------------------------- /lxd/db_storage_volumes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | _ "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | // Get config of a storage volume. 10 | func dbStorageVolumeConfigGet(db *sql.DB, volumeID int64) (map[string]string, error) { 11 | var key, value string 12 | query := "SELECT key, value FROM storage_volumes_config WHERE storage_volume_id=?" 13 | inargs := []interface{}{volumeID} 14 | outargs := []interface{}{key, value} 15 | 16 | results, err := dbQueryScan(db, query, inargs, outargs) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | config := map[string]string{} 22 | 23 | for _, r := range results { 24 | key = r[0].(string) 25 | value = r[1].(string) 26 | 27 | config[key] = value 28 | } 29 | 30 | return config, nil 31 | } 32 | 33 | // Get the description of a storage volume. 34 | func dbStorageVolumeDescriptionGet(db *sql.DB, volumeID int64) (string, error) { 35 | description := sql.NullString{} 36 | query := "SELECT description FROM storage_volumes WHERE id=?" 37 | inargs := []interface{}{volumeID} 38 | outargs := []interface{}{&description} 39 | 40 | err := dbQueryRowScan(db, query, inargs, outargs) 41 | if err != nil { 42 | if err == sql.ErrNoRows { 43 | return "", NoSuchObjectError 44 | } 45 | } 46 | 47 | return description.String, nil 48 | } 49 | 50 | // Update description of a storage volume. 51 | func dbStorageVolumeDescriptionUpdate(tx *sql.Tx, volumeID int64, description string) error { 52 | _, err := tx.Exec("UPDATE storage_volumes SET description=? WHERE id=?", description, volumeID) 53 | return err 54 | } 55 | 56 | // Add new storage volume config into database. 57 | func dbStorageVolumeConfigAdd(tx *sql.Tx, volumeID int64, volumeConfig map[string]string) error { 58 | str := "INSERT INTO storage_volumes_config (storage_volume_id, key, value) VALUES(?, ?, ?)" 59 | stmt, err := tx.Prepare(str) 60 | defer stmt.Close() 61 | 62 | for k, v := range volumeConfig { 63 | if v == "" { 64 | continue 65 | } 66 | 67 | _, err = stmt.Exec(volumeID, k, v) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // Delete storage volume config. 77 | func dbStorageVolumeConfigClear(tx *sql.Tx, volumeID int64) error { 78 | _, err := tx.Exec("DELETE FROM storage_volumes_config WHERE storage_volume_id=?", volumeID) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /doc/server.md: -------------------------------------------------------------------------------- 1 | # Server configuration 2 | The server configuration is a simple set of key and values. 3 | 4 | The key/value configuration is namespaced with the following namespaces 5 | currently supported: 6 | - core (core daemon configuration) 7 | - images (image configuration) 8 | 9 | Key | Type | Default | API extension | Description 10 | :-- | :--- | :------ | :------------ | :---------- 11 | core.https\_address | string | - | - | Address to bind for the remote API 12 | core.https\_allowed\_headers | string | - | - | Access-Control-Allow-Headers http header value 13 | core.https\_allowed\_methods | string | - | - | Access-Control-Allow-Methods http header value 14 | core.https\_allowed\_origin | string | - | - | Access-Control-Allow-Origin http header value 15 | core.https\_allowed\_credentials| boolean | - | - | Whether to set Access-Control-Allow-Credentials http header value to "true" 16 | core.proxy\_http | string | - | - | http proxy to use, if any (falls back to HTTP\_PROXY environment variable) 17 | core.proxy\_https | string | - | - | https proxy to use, if any (falls back to HTTPS\_PROXY environment variable) 18 | core.proxy\_ignore\_hosts | string | - | - | hosts which don't need the proxy for use (similar format to NO\_PROXY, e.g. 1.2.3.4,1.2.3.5, falls back to NO\_PROXY environment variable) 19 | core.trust\_password | string | - | - | Password to be provided by clients to setup a trust 20 | images.auto\_update\_cached | boolean | true | - | Whether to automatically update any image that LXD caches 21 | images.auto\_update\_interval | integer | 6 | - | Interval in hours at which to look for update to cached images (0 disables it) 22 | images.compression\_algorithm | string | gzip | - | Compression algorithm to use for new images (bzip2, gzip, lzma, xz or none) 23 | images.remote\_cache\_expiry | integer | 10 | - | Number of days after which an unused cached remote image will be flushed 24 | 25 | Those keys can be set using the lxc tool with: 26 | 27 | lxc config set 28 | -------------------------------------------------------------------------------- /doc/migration.md: -------------------------------------------------------------------------------- 1 | # Live Migration in LXD 2 | 3 | ## Overview 4 | 5 | Migration has two pieces, a "source", that is, the host that already has the 6 | container, and a "sink", the host that's getting the container. Currently, 7 | in the 'pull' mode, the source sets up an operation, and the sink connects 8 | to the source and pulls the container. 9 | 10 | There are three websockets (channels) used in migration: 11 | 1. the control stream 12 | 2. the criu images stream 13 | 3. the filesystem stream 14 | 15 | When a migration is initiated, information about the container, its 16 | configuration, etc. are sent over the control channel (a full 17 | description of this process is below), the criu images and container 18 | filesystem are synced over their respective channels, and the result of 19 | the restore operation is sent from the sink to the source over the 20 | control channel. 21 | 22 | In particular, the protocol that is spoken over the criu channel and filesystem 23 | channel can vary, depending on what is negotiated over the control socket. For 24 | example, both the source and the sink's LXD directory is on btrfs, the 25 | filesystem socket can speak btrfs-send/receive. Additionally, although we do a 26 | "stop the world" type migration right now, support for criu's p.haul protocol 27 | will happen over the criu socket at some later time. 28 | 29 | ## Control Socket 30 | 31 | Once all three websockets are connected between the two endpoints, the 32 | source sends a MigrationHeader (protobuf description found in 33 | `/lxd/migration/migrate.proto`). This header contains the container 34 | configuration which will be added to the new container. 35 | 36 | There are also two fields indicating the filesystem and criu protocol to speak. 37 | For example, if a server is hosted on a btrfs filesystem, it can indicate that it 38 | wants to do a `btrfs send` instead of a simple rsync (similarly, it could 39 | indicate that it wants to speak the p.haul protocol, instead of just rsyncing 40 | the images over slowly). 41 | 42 | The sink then examines this message and responds with whatever it 43 | supports. Continuing our example, if the sink is not on a btrfs 44 | filesystem, it responds with the lowest common denominator (rsync, in 45 | this case), and the source is to send the root filesystem using rsync. 46 | Similarly with the criu connection; if the sink doesn't have support for 47 | the p.haul protocol (or whatever), we fall back to rsync. 48 | -------------------------------------------------------------------------------- /shared/api/storage.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // StoragePoolsPost represents the fields of a new LXD storage pool 4 | // 5 | // API extension: storage 6 | type StoragePoolsPost struct { 7 | StoragePoolPut `yaml:",inline"` 8 | 9 | Name string `json:"name" yaml:"name"` 10 | Driver string `json:"driver" yaml:"driver"` 11 | } 12 | 13 | // StoragePool represents the fields of a LXD storage pool. 14 | // 15 | // API extension: storage 16 | type StoragePool struct { 17 | StoragePoolPut `yaml:",inline"` 18 | 19 | Name string `json:"name" yaml:"name"` 20 | Driver string `json:"driver" yaml:"driver"` 21 | UsedBy []string `json:"used_by" yaml:"used_by"` 22 | } 23 | 24 | // StoragePoolPut represents the modifiable fields of a LXD storage pool. 25 | // 26 | // API extension: storage 27 | type StoragePoolPut struct { 28 | Config map[string]string `json:"config" yaml:"config"` 29 | 30 | // API extension: entity_description 31 | Description string `json:"description" yaml:"description"` 32 | } 33 | 34 | // StorageVolumesPost represents the fields of a new LXD storage pool volume 35 | // 36 | // API extension: storage 37 | type StorageVolumesPost struct { 38 | StorageVolumePut `yaml:",inline"` 39 | 40 | Name string `json:"name" yaml:"name"` 41 | Type string `json:"type" yaml:"type"` 42 | } 43 | 44 | // StorageVolume represents the fields of a LXD storage volume. 45 | // 46 | // API extension: storage 47 | type StorageVolume struct { 48 | StorageVolumePut `yaml:",inline"` 49 | Name string `json:"name" yaml:"name"` 50 | Type string `json:"type" yaml:"type"` 51 | UsedBy []string `json:"used_by" yaml:"used_by"` 52 | } 53 | 54 | // StorageVolumePut represents the modifiable fields of a LXD storage volume. 55 | // 56 | // API extension: storage 57 | type StorageVolumePut struct { 58 | Config map[string]string `json:"config" yaml:"config"` 59 | 60 | // API extension: entity_description 61 | Description string `json:"description" yaml:"description"` 62 | } 63 | 64 | // Writable converts a full StoragePool struct into a StoragePoolPut struct 65 | // (filters read-only fields). 66 | func (storagePool *StoragePool) Writable() StoragePoolPut { 67 | return storagePool.StoragePoolPut 68 | } 69 | 70 | // Writable converts a full StorageVolume struct into a StorageVolumePut struct 71 | // (filters read-only fields). 72 | func (storageVolume *StorageVolume) Writable() StorageVolumePut { 73 | return storageVolume.StorageVolumePut 74 | } 75 | -------------------------------------------------------------------------------- /lxd/main_daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "os/signal" 8 | "runtime/pprof" 9 | "sync" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/lxc/lxd/shared" 14 | "github.com/lxc/lxd/shared/logger" 15 | ) 16 | 17 | func cmdDaemon() error { 18 | // Only root should run this 19 | if os.Geteuid() != 0 { 20 | return fmt.Errorf("This must be run as root") 21 | } 22 | 23 | if *argCPUProfile != "" { 24 | f, err := os.Create(*argCPUProfile) 25 | if err != nil { 26 | fmt.Printf("Error opening cpu profile file: %s\n", err) 27 | return nil 28 | } 29 | pprof.StartCPUProfile(f) 30 | defer pprof.StopCPUProfile() 31 | } 32 | 33 | if *argMemProfile != "" { 34 | go memProfiler(*argMemProfile) 35 | } 36 | 37 | neededPrograms := []string{"dnsmasq", "setfacl", "rsync", "tar", "unsquashfs", "xz"} 38 | for _, p := range neededPrograms { 39 | _, err := exec.LookPath(p) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | if *argPrintGoroutinesEvery > 0 { 46 | go func() { 47 | for { 48 | time.Sleep(time.Duration(*argPrintGoroutinesEvery) * time.Second) 49 | logger.Debugf(logger.GetStack()) 50 | } 51 | }() 52 | } 53 | 54 | d := &Daemon{ 55 | group: *argGroup, 56 | SetupMode: shared.PathExists(shared.VarPath(".setup_mode"))} 57 | err := d.Init() 58 | if err != nil { 59 | if d != nil && d.db != nil { 60 | d.db.Close() 61 | } 62 | return err 63 | } 64 | 65 | var ret error 66 | var wg sync.WaitGroup 67 | wg.Add(1) 68 | 69 | go func() { 70 | ch := make(chan os.Signal) 71 | signal.Notify(ch, syscall.SIGPWR) 72 | sig := <-ch 73 | 74 | logger.Infof("Received '%s signal', shutting down containers.", sig) 75 | 76 | containersShutdown(d) 77 | 78 | ret = d.Stop() 79 | wg.Done() 80 | }() 81 | 82 | go func() { 83 | <-d.shutdownChan 84 | 85 | logger.Infof("Asked to shutdown by API, shutting down containers.") 86 | 87 | containersShutdown(d) 88 | 89 | ret = d.Stop() 90 | wg.Done() 91 | }() 92 | 93 | go func() { 94 | ch := make(chan os.Signal) 95 | signal.Notify(ch, syscall.SIGINT) 96 | signal.Notify(ch, syscall.SIGQUIT) 97 | signal.Notify(ch, syscall.SIGTERM) 98 | sig := <-ch 99 | 100 | logger.Infof("Received '%s signal', exiting.", sig) 101 | ret = d.Stop() 102 | wg.Done() 103 | }() 104 | 105 | wg.Wait() 106 | return ret 107 | } 108 | -------------------------------------------------------------------------------- /test/suites/init_preseed.sh: -------------------------------------------------------------------------------- 1 | test_init_preseed() { 2 | # - lxd init --preseed 3 | lxd_backend=$(storage_backend "$LXD_DIR") 4 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 5 | chmod +x "${LXD_INIT_DIR}" 6 | spawn_lxd "${LXD_INIT_DIR}" false 7 | 8 | ( 9 | set -e 10 | # shellcheck disable=SC2034 11 | LXD_DIR=${LXD_INIT_DIR} 12 | 13 | # In case we're running against the ZFS backend, let's test 14 | # creating a zfs storage pool, otherwise just use dir. 15 | if [ "$lxd_backend" = "zfs" ]; then 16 | configure_loop_device loop_file_4 loop_device_4 17 | # shellcheck disable=SC2154 18 | zpool create "lxdtest-$(basename "${LXD_DIR}")-preseed-pool" "${loop_device_4}" -f -m none -O compression=on 19 | driver="zfs" 20 | source="lxdtest-$(basename "${LXD_DIR}")-preseed-pool" 21 | else 22 | driver="dir" 23 | source="" 24 | fi 25 | 26 | cat < /dev/null 2>&1; then 52 | sleep 2 53 | retries=$((retries-1)) 54 | continue 55 | fi 56 | break 57 | done 58 | 59 | if [ "${retries}" -eq 0 ]; then 60 | echo "First image ${fp1} not deleted from local store" 61 | return 1 62 | fi 63 | 64 | # The second image replaced the first one in the local storage. 65 | alias=$(lxc image info "${fp2}" | awk -F: '/^ Alias/ { print $2 }' | awk '{ print $1 }') 66 | [ "${alias}" = "testimage" ] 67 | 68 | lxc delete c1 69 | lxc remote remove l2 70 | lxc image delete "${fp2}" 71 | kill_lxd "$LXD2_DIR" 72 | } 73 | -------------------------------------------------------------------------------- /client/lxd_storage_pools.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared/api" 8 | ) 9 | 10 | // Storage pool handling functions 11 | 12 | // GetStoragePoolNames returns the names of all storage pools 13 | func (r *ProtocolLXD) GetStoragePoolNames() ([]string, error) { 14 | if !r.HasExtension("storage") { 15 | return nil, fmt.Errorf("The server is missing the required \"storage\" API extension") 16 | } 17 | 18 | urls := []string{} 19 | 20 | // Fetch the raw value 21 | _, err := r.queryStruct("GET", "/storage-pools", nil, "", &urls) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // Parse it 27 | names := []string{} 28 | for _, url := range urls { 29 | fields := strings.Split(url, "/storage-pools/") 30 | names = append(names, fields[len(fields)-1]) 31 | } 32 | 33 | return names, nil 34 | } 35 | 36 | // GetStoragePools returns a list of StoragePool entries 37 | func (r *ProtocolLXD) GetStoragePools() ([]api.StoragePool, error) { 38 | pools := []api.StoragePool{} 39 | 40 | // Fetch the raw value 41 | _, err := r.queryStruct("GET", "/storage-pools?recursion=1", nil, "", &pools) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return pools, nil 47 | } 48 | 49 | // GetStoragePool returns a StoragePool entry for the provided pool name 50 | func (r *ProtocolLXD) GetStoragePool(name string) (*api.StoragePool, string, error) { 51 | pool := api.StoragePool{} 52 | 53 | // Fetch the raw value 54 | etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s", name), nil, "", &pool) 55 | if err != nil { 56 | return nil, "", err 57 | } 58 | 59 | return &pool, etag, nil 60 | } 61 | 62 | // CreateStoragePool defines a new storage pool using the provided StoragePool struct 63 | func (r *ProtocolLXD) CreateStoragePool(pool api.StoragePoolsPost) error { 64 | if !r.HasExtension("storage") { 65 | return fmt.Errorf("The server is missing the required \"storage\" API extension") 66 | } 67 | 68 | // Send the request 69 | _, _, err := r.query("POST", "/storage-pools", pool, "") 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // UpdateStoragePool updates the pool to match the provided StoragePool struct 78 | func (r *ProtocolLXD) UpdateStoragePool(name string, pool api.StoragePoolPut, ETag string) error { 79 | // Send the request 80 | _, _, err := r.query("PUT", fmt.Sprintf("/storage-pools/%s", name), pool, ETag) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // DeleteStoragePool deletes a storage pool 89 | func (r *ProtocolLXD) DeleteStoragePool(name string) error { 90 | // Send the request 91 | _, _, err := r.query("DELETE", fmt.Sprintf("/storage-pools/%s", name), nil, "") 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /lxd/main_netcat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "os" 8 | "sync" 9 | "syscall" 10 | 11 | "github.com/lxc/lxd/shared" 12 | ) 13 | 14 | // Netcat is called with: 15 | // 16 | // lxd netcat /path/to/unix/socket 17 | // 18 | // and does unbuffered netcatting of to socket to stdin/stdout. Any arguments 19 | // after the path to the unix socket are ignored, so that this can be passed 20 | // directly to rsync as the sync command. 21 | func cmdNetcat(args []string) error { 22 | if len(args) < 3 { 23 | return fmt.Errorf("Bad arguments %q", args) 24 | } 25 | 26 | logPath := shared.LogPath(args[2], "netcat.log") 27 | if shared.PathExists(logPath) { 28 | os.Remove(logPath) 29 | } 30 | 31 | logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644) 32 | if err != nil { 33 | return err 34 | } 35 | defer logFile.Close() 36 | 37 | uAddr, err := net.ResolveUnixAddr("unix", args[1]) 38 | if err != nil { 39 | logFile.WriteString(fmt.Sprintf("Could not resolve unix domain socket \"%s\": %s.\n", args[1], err)) 40 | return err 41 | } 42 | 43 | conn, err := net.DialUnix("unix", nil, uAddr) 44 | if err != nil { 45 | logFile.WriteString(fmt.Sprintf("Could not dial unix domain socket \"%s\": %s.\n", args[1], err)) 46 | return err 47 | } 48 | 49 | wg := sync.WaitGroup{} 50 | wg.Add(1) 51 | 52 | go func() { 53 | _, err := io.Copy(eagainWriter{os.Stdout}, eagainReader{conn}) 54 | if err != nil { 55 | logFile.WriteString(fmt.Sprintf("Error while copying from stdout to unix domain socket \"%s\": %s.\n", args[1], err)) 56 | } 57 | conn.Close() 58 | wg.Done() 59 | }() 60 | 61 | go func() { 62 | _, err := io.Copy(eagainWriter{conn}, eagainReader{os.Stdin}) 63 | if err != nil { 64 | logFile.WriteString(fmt.Sprintf("Error while copying from unix domain socket \"%s\" to stdin: %s.\n", args[1], err)) 65 | } 66 | }() 67 | 68 | wg.Wait() 69 | 70 | return nil 71 | } 72 | 73 | type eagainReader struct { 74 | r io.Reader 75 | } 76 | 77 | func (er eagainReader) Read(p []byte) (int, error) { 78 | again: 79 | n, err := er.r.Read(p) 80 | if err == nil { 81 | return n, nil 82 | } 83 | 84 | // keep retrying on EAGAIN 85 | errno, ok := shared.GetErrno(err) 86 | if ok && errno == syscall.EAGAIN { 87 | goto again 88 | } 89 | 90 | return n, err 91 | } 92 | 93 | type eagainWriter struct { 94 | w io.Writer 95 | } 96 | 97 | func (ew eagainWriter) Write(p []byte) (int, error) { 98 | again: 99 | n, err := ew.w.Write(p) 100 | if err == nil { 101 | return n, nil 102 | } 103 | 104 | // keep retrying on EAGAIN 105 | errno, ok := shared.GetErrno(err) 106 | if ok && errno == syscall.EAGAIN { 107 | goto again 108 | } 109 | 110 | return n, err 111 | } 112 | -------------------------------------------------------------------------------- /client/lxd_profiles.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared/api" 8 | ) 9 | 10 | // Profile handling functions 11 | 12 | // GetProfileNames returns a list of available profile names 13 | func (r *ProtocolLXD) GetProfileNames() ([]string, error) { 14 | urls := []string{} 15 | 16 | // Fetch the raw value 17 | _, err := r.queryStruct("GET", "/profiles", nil, "", &urls) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | // Parse it 23 | names := []string{} 24 | for _, url := range urls { 25 | fields := strings.Split(url, "/profiles/") 26 | names = append(names, fields[len(fields)-1]) 27 | } 28 | 29 | return names, nil 30 | } 31 | 32 | // GetProfiles returns a list of available Profile structs 33 | func (r *ProtocolLXD) GetProfiles() ([]api.Profile, error) { 34 | profiles := []api.Profile{} 35 | 36 | // Fetch the raw value 37 | _, err := r.queryStruct("GET", "/profiles?recursion=1", nil, "", &profiles) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return profiles, nil 43 | } 44 | 45 | // GetProfile returns a Profile entry for the provided name 46 | func (r *ProtocolLXD) GetProfile(name string) (*api.Profile, string, error) { 47 | profile := api.Profile{} 48 | 49 | // Fetch the raw value 50 | etag, err := r.queryStruct("GET", fmt.Sprintf("/profiles/%s", name), nil, "", &profile) 51 | if err != nil { 52 | return nil, "", err 53 | } 54 | 55 | return &profile, etag, nil 56 | } 57 | 58 | // CreateProfile defines a new container profile 59 | func (r *ProtocolLXD) CreateProfile(profile api.ProfilesPost) error { 60 | // Send the request 61 | _, _, err := r.query("POST", "/profiles", profile, "") 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // UpdateProfile updates the profile to match the provided Profile struct 70 | func (r *ProtocolLXD) UpdateProfile(name string, profile api.ProfilePut, ETag string) error { 71 | // Send the request 72 | _, _, err := r.query("PUT", fmt.Sprintf("/profiles/%s", name), profile, ETag) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // RenameProfile renames an existing profile entry 81 | func (r *ProtocolLXD) RenameProfile(name string, profile api.ProfilePost) error { 82 | // Send the request 83 | _, _, err := r.query("POST", fmt.Sprintf("/profiles/%s", name), profile, "") 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // DeleteProfile deletes a profile 92 | func (r *ProtocolLXD) DeleteProfile(name string) error { 93 | // Send the request 94 | _, _, err := r.query("DELETE", fmt.Sprintf("/profiles/%s", name), nil, "") 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /client/lxd_certificates.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared/api" 8 | ) 9 | 10 | // Certificate handling functions 11 | 12 | // GetCertificateFingerprints returns a list of certificate fingerprints 13 | func (r *ProtocolLXD) GetCertificateFingerprints() ([]string, error) { 14 | certificates := []string{} 15 | 16 | // Fetch the raw value 17 | _, err := r.queryStruct("GET", "/certificates", nil, "", &certificates) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | // Parse it 23 | fingerprints := []string{} 24 | for _, fingerprint := range fingerprints { 25 | fields := strings.Split(fingerprint, "/certificates/") 26 | fingerprints = append(fingerprints, fields[len(fields)-1]) 27 | } 28 | 29 | return fingerprints, nil 30 | } 31 | 32 | // GetCertificates returns a list of certificates 33 | func (r *ProtocolLXD) GetCertificates() ([]api.Certificate, error) { 34 | certificates := []api.Certificate{} 35 | 36 | // Fetch the raw value 37 | _, err := r.queryStruct("GET", "/certificates?recursion=1", nil, "", &certificates) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return certificates, nil 43 | } 44 | 45 | // GetCertificate returns the certificate entry for the provided fingerprint 46 | func (r *ProtocolLXD) GetCertificate(fingerprint string) (*api.Certificate, string, error) { 47 | certificate := api.Certificate{} 48 | 49 | // Fetch the raw value 50 | etag, err := r.queryStruct("GET", fmt.Sprintf("/certificates/%s", fingerprint), nil, "", &certificate) 51 | if err != nil { 52 | return nil, "", err 53 | } 54 | 55 | return &certificate, etag, nil 56 | } 57 | 58 | // CreateCertificate adds a new certificate to the LXD trust store 59 | func (r *ProtocolLXD) CreateCertificate(certificate api.CertificatesPost) error { 60 | // Send the request 61 | _, _, err := r.query("POST", "/certificates", certificate, "") 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // UpdateCertificate updates the certificate definition 70 | func (r *ProtocolLXD) UpdateCertificate(fingerprint string, certificate api.CertificatePut, ETag string) error { 71 | if !r.HasExtension("certificate_update") { 72 | return fmt.Errorf("The server is missing the required \"certificate_update\" API extension") 73 | } 74 | 75 | // Send the request 76 | _, _, err := r.query("PUT", fmt.Sprintf("/certificates/%s", fingerprint), certificate, ETag) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // DeleteCertificate removes a certificate from the LXD trust store 85 | func (r *ProtocolLXD) DeleteCertificate(fingerprint string) error { 86 | // Send the request 87 | _, _, err := r.query("DELETE", fmt.Sprintf("/certificates/%s", fingerprint), nil, "") 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /test/suites/init_auto.sh: -------------------------------------------------------------------------------- 1 | test_init_auto() { 2 | # - lxd init --auto --storage-backend zfs 3 | # and 4 | # - lxd init --auto 5 | # can't be easily tested on jenkins since it hard-codes "default" as pool 6 | # naming. This can cause naming conflicts when multiple test-suites are run on 7 | # a single runner. 8 | 9 | if [ "$(storage_backend "$LXD_DIR")" = "zfs" ]; then 10 | # lxd init --auto --storage-backend zfs --storage-pool 11 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 12 | chmod +x "${LXD_INIT_DIR}" 13 | spawn_lxd "${LXD_INIT_DIR}" false 14 | 15 | configure_loop_device loop_file_1 loop_device_1 16 | # shellcheck disable=SC2154 17 | zpool create "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool" "${loop_device_1}" -m none -O compression=on 18 | LXD_DIR=${LXD_INIT_DIR} lxd init --auto --storage-backend zfs --storage-pool "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool" 19 | LXD_DIR=${LXD_INIT_DIR} lxc profile show default | grep -q "pool: default" 20 | 21 | kill_lxd "${LXD_INIT_DIR}" 22 | sed -i "\|^${loop_device_1}|d" "${TEST_DIR}/loops" 23 | 24 | # lxd init --auto --storage-backend zfs --storage-pool / 25 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 26 | chmod +x "${LXD_INIT_DIR}" 27 | spawn_lxd "${LXD_INIT_DIR}" false 28 | 29 | # shellcheck disable=SC2154 30 | configure_loop_device loop_file_1 loop_device_1 31 | zpool create "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool" "${loop_device_1}" -m none -O compression=on 32 | LXD_DIR=${LXD_INIT_DIR} lxd init --auto --storage-backend zfs --storage-pool "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool/non-existing-dataset" 33 | 34 | kill_lxd "${LXD_INIT_DIR}" 35 | 36 | # lxd init --auto --storage-backend zfs --storage-pool / 37 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 38 | chmod +x "${LXD_INIT_DIR}" 39 | spawn_lxd "${LXD_INIT_DIR}" false 40 | 41 | zfs create -p -o mountpoint=none "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool/existing-dataset" 42 | LXD_DIR=${LXD_INIT_DIR} lxd init --auto --storage-backend zfs --storage-pool "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool/existing-dataset" 43 | 44 | kill_lxd "${LXD_INIT_DIR}" 45 | zpool destroy "lxdtest-$(basename "${LXD_DIR}")-pool1-existing-pool" 46 | sed -i "\|^${loop_device_1}|d" "${TEST_DIR}/loops" 47 | 48 | # lxd init --storage-backend zfs --storage-create-loop 1 --storage-pool --auto 49 | LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) 50 | chmod +x "${LXD_INIT_DIR}" 51 | spawn_lxd "${LXD_INIT_DIR}" false 52 | 53 | ZFS_POOL="lxdtest-$(basename "${LXD_DIR}")-init" 54 | LXD_DIR=${LXD_INIT_DIR} lxd init --storage-backend zfs --storage-create-loop 1 --storage-pool "${ZFS_POOL}" --auto 55 | 56 | kill_lxd "${LXD_INIT_DIR}" 57 | fi 58 | } 59 | -------------------------------------------------------------------------------- /client/events.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // The EventListener struct is used to interact with a LXD event stream 9 | type EventListener struct { 10 | r *ProtocolLXD 11 | chActive chan bool 12 | disconnected bool 13 | err error 14 | 15 | targets []*EventTarget 16 | targetsLock sync.Mutex 17 | } 18 | 19 | // The EventTarget struct is returned to the caller of AddHandler and used in RemoveHandler 20 | type EventTarget struct { 21 | function func(interface{}) 22 | types []string 23 | } 24 | 25 | // AddHandler adds a function to be called whenever an event is received 26 | func (e *EventListener) AddHandler(types []string, function func(interface{})) (*EventTarget, error) { 27 | if function == nil { 28 | return nil, fmt.Errorf("A valid function must be provided") 29 | } 30 | 31 | // Handle locking 32 | e.targetsLock.Lock() 33 | defer e.targetsLock.Unlock() 34 | 35 | // Create a new target 36 | target := EventTarget{ 37 | function: function, 38 | types: types, 39 | } 40 | 41 | // And add it to the targets 42 | e.targets = append(e.targets, &target) 43 | 44 | return &target, nil 45 | } 46 | 47 | // RemoveHandler removes a function to be called whenever an event is received 48 | func (e *EventListener) RemoveHandler(target *EventTarget) error { 49 | if target == nil { 50 | return fmt.Errorf("A valid event target must be provided") 51 | } 52 | 53 | // Handle locking 54 | e.targetsLock.Lock() 55 | defer e.targetsLock.Unlock() 56 | 57 | // Locate and remove the function from the list 58 | for i, entry := range e.targets { 59 | if entry == target { 60 | copy(e.targets[i:], e.targets[i+1:]) 61 | e.targets[len(e.targets)-1] = nil 62 | e.targets = e.targets[:len(e.targets)-1] 63 | return nil 64 | } 65 | } 66 | 67 | return fmt.Errorf("Couldn't find this function and event types combination") 68 | } 69 | 70 | // Disconnect must be used once done listening for events 71 | func (e *EventListener) Disconnect() { 72 | if e.disconnected { 73 | return 74 | } 75 | 76 | // Handle locking 77 | e.r.eventListenersLock.Lock() 78 | defer e.r.eventListenersLock.Unlock() 79 | 80 | // Locate and remove it from the global list 81 | for i, listener := range e.r.eventListeners { 82 | if listener == e { 83 | copy(e.r.eventListeners[i:], e.r.eventListeners[i+1:]) 84 | e.r.eventListeners[len(e.r.eventListeners)-1] = nil 85 | e.r.eventListeners = e.r.eventListeners[:len(e.r.eventListeners)-1] 86 | break 87 | } 88 | } 89 | 90 | // Turn off the handler 91 | e.err = nil 92 | e.disconnected = true 93 | close(e.chActive) 94 | } 95 | 96 | // Wait hangs until the server disconnects the connection or Disconnect() is called 97 | func (e *EventListener) Wait() error { 98 | <-e.chActive 99 | return e.err 100 | } 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pull requests: 2 | 3 | Changes to this project should be proposed as pull requests on Github 4 | at: https://github.com/lxc/lxd 5 | 6 | Proposed changes will then go through code review there and once acked, 7 | be merged in the main branch. 8 | 9 | 10 | # License and copyright: 11 | 12 | By default, any contribution to this project is made under the Apache 13 | 2.0 license. 14 | 15 | The author of a change remains the copyright holder of their code 16 | (no copyright assignment). 17 | 18 | 19 | # Developer Certificate of Origin: 20 | 21 | To improve tracking of contributions to this project we use the DCO 1.1 22 | and use a "sign-off" procedure for all changes going into the branch. 23 | 24 | The sign-off is a simple line at the end of the explanation for the 25 | commit which certifies that you wrote it or otherwise have the right 26 | to pass it on as an open-source contribution. 27 | 28 | > Developer Certificate of Origin 29 | > Version 1.1 30 | > 31 | > Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 32 | > 660 York Street, Suite 102, 33 | > San Francisco, CA 94110 USA 34 | > 35 | > Everyone is permitted to copy and distribute verbatim copies of this 36 | > license document, but changing it is not allowed. 37 | > 38 | > Developer's Certificate of Origin 1.1 39 | > 40 | > By making a contribution to this project, I certify that: 41 | > 42 | > (a) The contribution was created in whole or in part by me and I 43 | > have the right to submit it under the open source license 44 | > indicated in the file; or 45 | > 46 | > (b) The contribution is based upon previous work that, to the best 47 | > of my knowledge, is covered under an appropriate open source 48 | > license and I have the right under that license to submit that 49 | > work with modifications, whether created in whole or in part 50 | > by me, under the same open source license (unless I am 51 | > permitted to submit under a different license), as indicated 52 | > in the file; or 53 | > 54 | > (c) The contribution was provided directly to me by some other 55 | > person who certified (a), (b) or (c) and I have not modified 56 | > it. 57 | > 58 | > (d) I understand and agree that this project and the contribution 59 | > are public and that a record of the contribution (including all 60 | > personal information I submit with it, including my sign-off) is 61 | > maintained indefinitely and may be redistributed consistent with 62 | > this project or the open source license(s) involved. 63 | 64 | An example of a valid sign-off line is: 65 | 66 | Signed-off-by: Random J Developer 67 | 68 | Use your real name and a valid e-mail address. 69 | Sorry, no pseudonyms or anonymous contributions are allowed. 70 | 71 | We also require each commit be individually signed-off by their author, 72 | even when part of a larger set. You may find `git commit -s` useful. 73 | -------------------------------------------------------------------------------- /client/lxd_networks.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared/api" 8 | ) 9 | 10 | // GetNetworkNames returns a list of network names 11 | func (r *ProtocolLXD) GetNetworkNames() ([]string, error) { 12 | urls := []string{} 13 | 14 | // Fetch the raw value 15 | _, err := r.queryStruct("GET", "/networks", nil, "", &urls) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | // Parse it 21 | names := []string{} 22 | for _, url := range urls { 23 | fields := strings.Split(url, "/networks/") 24 | names = append(names, fields[len(fields)-1]) 25 | } 26 | 27 | return names, nil 28 | } 29 | 30 | // GetNetworks returns a list of Network struct 31 | func (r *ProtocolLXD) GetNetworks() ([]api.Network, error) { 32 | networks := []api.Network{} 33 | 34 | // Fetch the raw value 35 | _, err := r.queryStruct("GET", "/networks?recursion=1", nil, "", &networks) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return networks, nil 41 | } 42 | 43 | // GetNetwork returns a Network entry for the provided name 44 | func (r *ProtocolLXD) GetNetwork(name string) (*api.Network, string, error) { 45 | network := api.Network{} 46 | 47 | // Fetch the raw value 48 | etag, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s", name), nil, "", &network) 49 | if err != nil { 50 | return nil, "", err 51 | } 52 | 53 | return &network, etag, nil 54 | } 55 | 56 | // CreateNetwork defines a new network using the provided Network struct 57 | func (r *ProtocolLXD) CreateNetwork(network api.NetworksPost) error { 58 | if !r.HasExtension("network") { 59 | return fmt.Errorf("The server is missing the required \"network\" API extension") 60 | } 61 | 62 | // Send the request 63 | _, _, err := r.query("POST", "/networks", network, "") 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // UpdateNetwork updates the network to match the provided Network struct 72 | func (r *ProtocolLXD) UpdateNetwork(name string, network api.NetworkPut, ETag string) error { 73 | // Send the request 74 | _, _, err := r.query("PUT", fmt.Sprintf("/networks/%s", name), network, ETag) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // RenameNetwork renames an existing network entry 83 | func (r *ProtocolLXD) RenameNetwork(name string, network api.NetworkPost) error { 84 | // Send the request 85 | _, _, err := r.query("POST", fmt.Sprintf("/networks/%s", name), network, "") 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // DeleteNetwork deletes an existing network 94 | func (r *ProtocolLXD) DeleteNetwork(name string) error { 95 | // Send the request 96 | _, _, err := r.query("DELETE", fmt.Sprintf("/networks/%s", name), nil, "") 97 | if err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /lxd/container_logs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gorilla/mux" 11 | 12 | "github.com/lxc/lxd/shared" 13 | "github.com/lxc/lxd/shared/version" 14 | ) 15 | 16 | func containerLogsGet(d *Daemon, r *http.Request) Response { 17 | /* Let's explicitly *not* try to do a containerLoadByName here. In some 18 | * cases (e.g. when container creation failed), the container won't 19 | * exist in the DB but it does have some log files on disk. 20 | * 21 | * However, we should check this name and ensure it's a valid container 22 | * name just so that people can't list arbitrary directories. 23 | */ 24 | name := mux.Vars(r)["name"] 25 | 26 | if err := containerValidName(name); err != nil { 27 | return BadRequest(err) 28 | } 29 | 30 | result := []string{} 31 | 32 | dents, err := ioutil.ReadDir(shared.LogPath(name)) 33 | if err != nil { 34 | return SmartError(err) 35 | } 36 | 37 | for _, f := range dents { 38 | if !validLogFileName(f.Name()) { 39 | continue 40 | } 41 | 42 | result = append(result, fmt.Sprintf("/%s/containers/%s/logs/%s", version.APIVersion, name, f.Name())) 43 | } 44 | 45 | return SyncResponse(true, result) 46 | } 47 | 48 | var containerLogsCmd = Command{ 49 | name: "containers/{name}/logs", 50 | get: containerLogsGet, 51 | } 52 | 53 | func validLogFileName(fname string) bool { 54 | /* Let's just require that the paths be relative, so that we don't have 55 | * to deal with any escaping or whatever. 56 | */ 57 | return fname == "lxc.log" || 58 | fname == "lxc.conf" || 59 | strings.HasPrefix(fname, "migration_") || 60 | strings.HasPrefix(fname, "snapshot_") || 61 | strings.HasPrefix(fname, "exec_") 62 | } 63 | 64 | func containerLogGet(d *Daemon, r *http.Request) Response { 65 | name := mux.Vars(r)["name"] 66 | file := mux.Vars(r)["file"] 67 | 68 | if err := containerValidName(name); err != nil { 69 | return BadRequest(err) 70 | } 71 | 72 | if !validLogFileName(file) { 73 | return BadRequest(fmt.Errorf("log file name %s not valid", file)) 74 | } 75 | 76 | ent := fileResponseEntry{ 77 | path: shared.LogPath(name, file), 78 | filename: file, 79 | } 80 | 81 | return FileResponse(r, []fileResponseEntry{ent}, nil, false) 82 | } 83 | 84 | func containerLogDelete(d *Daemon, r *http.Request) Response { 85 | name := mux.Vars(r)["name"] 86 | file := mux.Vars(r)["file"] 87 | 88 | if err := containerValidName(name); err != nil { 89 | return BadRequest(err) 90 | } 91 | 92 | if !validLogFileName(file) { 93 | return BadRequest(fmt.Errorf("log file name %s not valid", file)) 94 | } 95 | 96 | return SmartError(os.Remove(shared.LogPath(name, file))) 97 | } 98 | 99 | var containerLogCmd = Command{ 100 | name: "containers/{name}/logs/{file}", 101 | get: containerLogGet, 102 | delete: containerLogDelete, 103 | } 104 | -------------------------------------------------------------------------------- /client/lxd_events.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/lxc/lxd/shared" 7 | ) 8 | 9 | // Event handling functions 10 | 11 | // GetEvents connects to the LXD monitoring interface 12 | func (r *ProtocolLXD) GetEvents() (*EventListener, error) { 13 | // Prevent anything else from interacting with the listeners 14 | r.eventListenersLock.Lock() 15 | defer r.eventListenersLock.Unlock() 16 | 17 | // Setup a new listener 18 | listener := EventListener{ 19 | r: r, 20 | chActive: make(chan bool), 21 | } 22 | 23 | if r.eventListeners != nil { 24 | // There is an existing Go routine setup, so just add another target 25 | r.eventListeners = append(r.eventListeners, &listener) 26 | return &listener, nil 27 | } 28 | 29 | // Initialize the list if needed 30 | r.eventListeners = []*EventListener{} 31 | 32 | // Setup a new connection with LXD 33 | conn, err := r.websocket("/events") 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // Add the listener 39 | r.eventListeners = append(r.eventListeners, &listener) 40 | 41 | // And spawn the listener 42 | go func() { 43 | for { 44 | r.eventListenersLock.Lock() 45 | if len(r.eventListeners) == 0 { 46 | // We don't need the connection anymore, disconnect 47 | conn.Close() 48 | 49 | r.eventListeners = nil 50 | r.eventListenersLock.Unlock() 51 | break 52 | } 53 | r.eventListenersLock.Unlock() 54 | 55 | _, data, err := conn.ReadMessage() 56 | if err != nil { 57 | // Prevent anything else from interacting with the listeners 58 | r.eventListenersLock.Lock() 59 | defer r.eventListenersLock.Unlock() 60 | 61 | // Tell all the current listeners about the failure 62 | for _, listener := range r.eventListeners { 63 | listener.err = err 64 | listener.disconnected = true 65 | close(listener.chActive) 66 | } 67 | 68 | // And remove them all from the list 69 | r.eventListeners = []*EventListener{} 70 | return 71 | } 72 | 73 | // Attempt to unpack the message 74 | message := make(map[string]interface{}) 75 | err = json.Unmarshal(data, &message) 76 | if err != nil { 77 | continue 78 | } 79 | 80 | // Extract the message type 81 | _, ok := message["type"] 82 | if !ok { 83 | continue 84 | } 85 | messageType := message["type"].(string) 86 | 87 | // Send the message to all handlers 88 | r.eventListenersLock.Lock() 89 | for _, listener := range r.eventListeners { 90 | listener.targetsLock.Lock() 91 | for _, target := range listener.targets { 92 | if target.types != nil && !shared.StringInSlice(messageType, target.types) { 93 | continue 94 | } 95 | 96 | go target.function(message) 97 | } 98 | listener.targetsLock.Unlock() 99 | } 100 | r.eventListenersLock.Unlock() 101 | } 102 | }() 103 | 104 | return &listener, nil 105 | } 106 | -------------------------------------------------------------------------------- /lxd/container_patch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/gorilla/mux" 11 | 12 | "github.com/lxc/lxd/shared" 13 | "github.com/lxc/lxd/shared/api" 14 | "github.com/lxc/lxd/shared/osarch" 15 | ) 16 | 17 | func containerPatch(d *Daemon, r *http.Request) Response { 18 | // Get the container 19 | name := mux.Vars(r)["name"] 20 | c, err := containerLoadByName(d, name) 21 | if err != nil { 22 | return NotFound 23 | } 24 | 25 | // Validate the ETag 26 | etag := []interface{}{c.Architecture(), c.LocalConfig(), c.LocalDevices(), c.IsEphemeral(), c.Profiles()} 27 | err = etagCheck(r, etag) 28 | if err != nil { 29 | return PreconditionFailed(err) 30 | } 31 | 32 | body, err := ioutil.ReadAll(r.Body) 33 | if err != nil { 34 | return InternalError(err) 35 | } 36 | 37 | rdr1 := ioutil.NopCloser(bytes.NewBuffer(body)) 38 | rdr2 := ioutil.NopCloser(bytes.NewBuffer(body)) 39 | 40 | reqRaw := shared.Jmap{} 41 | if err := json.NewDecoder(rdr1).Decode(&reqRaw); err != nil { 42 | return BadRequest(err) 43 | } 44 | 45 | req := api.ContainerPut{} 46 | if err := json.NewDecoder(rdr2).Decode(&req); err != nil { 47 | return BadRequest(err) 48 | } 49 | 50 | if req.Restore != "" { 51 | return BadRequest(fmt.Errorf("Can't call PATCH in restore mode.")) 52 | } 53 | 54 | // Check if architecture was passed 55 | var architecture int 56 | _, err = reqRaw.GetString("architecture") 57 | if err != nil { 58 | architecture = c.Architecture() 59 | } else { 60 | architecture, err = osarch.ArchitectureId(req.Architecture) 61 | if err != nil { 62 | architecture = 0 63 | } 64 | } 65 | 66 | // Check if ephemeral was passed 67 | _, err = reqRaw.GetBool("ephemeral") 68 | if err != nil { 69 | req.Ephemeral = c.IsEphemeral() 70 | } 71 | 72 | // Check if profiles was passed 73 | if req.Profiles == nil { 74 | req.Profiles = c.Profiles() 75 | } 76 | 77 | // Check if config was passed 78 | if req.Config == nil { 79 | req.Config = c.LocalConfig() 80 | } else { 81 | for k, v := range c.LocalConfig() { 82 | _, ok := req.Config[k] 83 | if !ok { 84 | req.Config[k] = v 85 | } 86 | } 87 | } 88 | 89 | // Check if devices was passed 90 | if req.Devices == nil { 91 | req.Devices = c.LocalDevices() 92 | } else { 93 | for k, v := range c.LocalDevices() { 94 | _, ok := req.Devices[k] 95 | if !ok { 96 | req.Devices[k] = v 97 | } 98 | } 99 | } 100 | 101 | // Update container configuration 102 | args := containerArgs{ 103 | Architecture: architecture, 104 | Description: req.Description, 105 | Config: req.Config, 106 | Devices: req.Devices, 107 | Ephemeral: req.Ephemeral, 108 | Profiles: req.Profiles} 109 | 110 | err = c.Update(args, false) 111 | if err != nil { 112 | return SmartError(err) 113 | } 114 | 115 | return EmptySyncResponse 116 | } 117 | -------------------------------------------------------------------------------- /doc/dev-lxd.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Communication between the hosted workload (container) and its host while 3 | not strictly needed is a pretty useful feature. 4 | 5 | In LXD, this feature is implemented through a /dev/lxd/sock node which is 6 | created and setup for all LXD containers. 7 | 8 | This file is a Unix socket which processes inside the container can 9 | connect to. It's multi-threaded so multiple clients can be connected at the 10 | same time. 11 | 12 | # Implementation details 13 | LXD on the host binds /var/lib/lxd/devlxd and starts listening for new 14 | connections on it. 15 | 16 | This socket is then bind-mounted into every single container started by 17 | LXD at /dev/lxd/sock. 18 | 19 | The bind-mount is required so we can exceed 4096 containers, otherwise, 20 | LXD would have to bind a different socket for every container, quickly 21 | reaching the FD limit. 22 | 23 | # Authentication 24 | Queries on /dev/lxd/sock will only return information related to the 25 | requesting container. To figure out where a request comes from, LXD will 26 | extract the initial socket ucred and compare that to the list of 27 | containers it manages. 28 | 29 | # Protocol 30 | The protocol on /dev/lxd/sock is plain-text HTTP with JSON messaging, so very 31 | similar to the local version of the LXD protocol. 32 | 33 | Unlike the main LXD API, there is no background operation and no 34 | authentication support in the /dev/lxd/sock API. 35 | 36 | # REST-API 37 | ## API structure 38 | * / 39 | * /1.0 40 | * /1.0/config 41 | * /1.0/config/{key} 42 | * /1.0/meta-data 43 | 44 | ## API details 45 | ### / 46 | #### GET 47 | * Description: List of supported APIs 48 | * Return: list of supported API endpoint URLs (by default ['/1.0']) 49 | 50 | Return value: 51 | 52 | [ 53 | "/1.0" 54 | ] 55 | 56 | ### /1.0 57 | #### GET 58 | * Description: Information about the 1.0 API 59 | * Return: dict 60 | 61 | Return value: 62 | 63 | { 64 | "api_version": "1.0" 65 | } 66 | 67 | ### /1.0/config 68 | #### GET 69 | * Description: List of configuration keys 70 | * Return: list of configuration keys URL 71 | 72 | Note that the configuration key names match those in the container 73 | config, however not all configuration namespaces will be exported to 74 | /dev/lxd/sock. 75 | Currently only the user.\* keys are accessible to the container. 76 | 77 | At this time, there also aren't any container-writable namespace. 78 | 79 | Return value: 80 | 81 | [ 82 | "/1.0/config/user.a" 83 | ] 84 | 85 | ### /1.0/config/\ 86 | #### GET 87 | * Description: Value of that key 88 | * Return: Plain-text value 89 | 90 | Return value: 91 | 92 | blah 93 | 94 | ### /1.0/meta-data 95 | #### GET 96 | * Description: Container meta-data compatible with cloud-init 97 | * Return: cloud-init meta-data 98 | 99 | Return value: 100 | 101 | #cloud-config 102 | instance-id: abc 103 | local-hostname: abc 104 | -------------------------------------------------------------------------------- /test/suites/template.sh: -------------------------------------------------------------------------------- 1 | test_template() { 2 | # Import a template which only triggers on create 3 | deps/import-busybox --alias template-test --template create 4 | lxc init template-test template 5 | 6 | # Confirm that template application is delayed to first start 7 | ! lxc file pull template/template - 8 | 9 | # Validate that the template is applied 10 | lxc start template 11 | lxc file pull template/template - | grep "^name: template$" 12 | 13 | # Confirm it's not applied on copies 14 | lxc copy template template1 15 | lxc file pull template1/template - | grep "^name: template$" 16 | 17 | # Cleanup 18 | lxc image delete template-test 19 | lxc delete template template1 --force 20 | 21 | 22 | # Import a template which only triggers on copy 23 | deps/import-busybox --alias template-test --template copy 24 | lxc launch template-test template 25 | 26 | # Confirm that the template doesn't trigger on create 27 | ! lxc file pull template/template - 28 | 29 | # Copy the container 30 | lxc copy template template1 31 | 32 | # Confirm that template application is delayed to first start 33 | ! lxc file pull template1/template - 34 | 35 | # Validate that the template is applied 36 | lxc start template1 37 | lxc file pull template1/template - | grep "^name: template1$" 38 | 39 | # Cleanup 40 | lxc image delete template-test 41 | lxc delete template template1 --force 42 | 43 | 44 | # Import a template which only triggers on start 45 | deps/import-busybox --alias template-test --template start 46 | lxc launch template-test template 47 | 48 | # Validate that the template is applied 49 | lxc file pull template/template - | grep "^name: template$" 50 | lxc file pull template/template - | grep "^user.foo: _unset_$" 51 | 52 | # Confirm it's re-run at every start 53 | lxc config set template user.foo bar 54 | lxc restart template --force 55 | lxc file pull template/template - | grep "^user.foo: bar$" 56 | 57 | # Cleanup 58 | lxc image delete template-test 59 | lxc delete template --force 60 | 61 | 62 | # Import a template which triggers on both create and copy 63 | deps/import-busybox --alias template-test --template create,copy 64 | lxc init template-test template 65 | 66 | # Confirm that template application is delayed to first start 67 | ! lxc file pull template/template - 68 | 69 | # Validate that the template is applied 70 | lxc start template 71 | lxc file pull template/template - | grep "^name: template$" 72 | 73 | # Confirm it's also applied on copies 74 | lxc copy template template1 75 | lxc start template1 76 | lxc file pull template1/template - | grep "^name: template1$" 77 | lxc file pull template1/template - | grep "^user.foo: _unset_$" 78 | 79 | # But doesn't change on restart 80 | lxc config set template1 user.foo bar 81 | lxc restart template1 --force 82 | lxc file pull template1/template - | grep "^user.foo: _unset_$" 83 | 84 | # Cleanup 85 | lxc image delete template-test 86 | lxc delete template template1 --force 87 | } 88 | -------------------------------------------------------------------------------- /shared/logger/log.go: -------------------------------------------------------------------------------- 1 | // +build !logdebug 2 | 3 | package logger 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // Logger is the main logging interface 10 | type Logger interface { 11 | Debug(msg string, ctx ...interface{}) 12 | Info(msg string, ctx ...interface{}) 13 | Warn(msg string, ctx ...interface{}) 14 | Error(msg string, ctx ...interface{}) 15 | Crit(msg string, ctx ...interface{}) 16 | } 17 | 18 | // Log contains the logger used by all the logging functions 19 | var Log Logger 20 | 21 | type nullLogger struct{} 22 | 23 | func (nl nullLogger) Debug(msg string, ctx ...interface{}) {} 24 | func (nl nullLogger) Info(msg string, ctx ...interface{}) {} 25 | func (nl nullLogger) Warn(msg string, ctx ...interface{}) {} 26 | func (nl nullLogger) Error(msg string, ctx ...interface{}) {} 27 | func (nl nullLogger) Crit(msg string, ctx ...interface{}) {} 28 | 29 | func init() { 30 | Log = nullLogger{} 31 | } 32 | 33 | // Debug logs a message (with optional context) at the DEBUG log level 34 | func Debug(msg string, ctx ...interface{}) { 35 | if Log != nil { 36 | Log.Debug(msg, ctx...) 37 | } 38 | } 39 | 40 | // Info logs a message (with optional context) at the INFO log level 41 | func Info(msg string, ctx ...interface{}) { 42 | if Log != nil { 43 | Log.Info(msg, ctx...) 44 | } 45 | } 46 | 47 | // Warn logs a message (with optional context) at the WARNING log level 48 | func Warn(msg string, ctx ...interface{}) { 49 | if Log != nil { 50 | Log.Warn(msg, ctx...) 51 | } 52 | } 53 | 54 | // Error logs a message (with optional context) at the ERROR log level 55 | func Error(msg string, ctx ...interface{}) { 56 | if Log != nil { 57 | Log.Error(msg, ctx...) 58 | } 59 | } 60 | 61 | // Crit logs a message (with optional context) at the CRITICAL log level 62 | func Crit(msg string, ctx ...interface{}) { 63 | if Log != nil { 64 | Log.Crit(msg, ctx...) 65 | } 66 | } 67 | 68 | // Infof logs at the INFO log level using a standard printf format string 69 | func Infof(format string, args ...interface{}) { 70 | if Log != nil { 71 | Log.Info(fmt.Sprintf(format, args...)) 72 | } 73 | } 74 | 75 | // Debugf logs at the DEBUG log level using a standard printf format string 76 | func Debugf(format string, args ...interface{}) { 77 | if Log != nil { 78 | Log.Debug(fmt.Sprintf(format, args...)) 79 | } 80 | } 81 | 82 | // Warnf logs at the WARNING log level using a standard printf format string 83 | func Warnf(format string, args ...interface{}) { 84 | if Log != nil { 85 | Log.Warn(fmt.Sprintf(format, args...)) 86 | } 87 | } 88 | 89 | // Errorf logs at the ERROR log level using a standard printf format string 90 | func Errorf(format string, args ...interface{}) { 91 | if Log != nil { 92 | Log.Error(fmt.Sprintf(format, args...)) 93 | } 94 | } 95 | 96 | // Critf logs at the CRITICAL log level using a standard printf format string 97 | func Critf(format string, args ...interface{}) { 98 | if Log != nil { 99 | Log.Crit(fmt.Sprintf(format, args...)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/lxd_storage_volumes.go: -------------------------------------------------------------------------------- 1 | package lxd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared/api" 8 | ) 9 | 10 | // Storage volumes handling function 11 | 12 | // GetStoragePoolVolumeNames returns the names of all volumes in a pool 13 | func (r *ProtocolLXD) GetStoragePoolVolumeNames(pool string) ([]string, error) { 14 | if !r.HasExtension("storage") { 15 | return nil, fmt.Errorf("The server is missing the required \"storage\" API extension") 16 | } 17 | 18 | urls := []string{} 19 | 20 | // Fetch the raw value 21 | _, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes", pool), nil, "", &urls) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // Parse it 27 | names := []string{} 28 | for _, url := range urls { 29 | fields := strings.Split(url, fmt.Sprintf("/storage-pools/%s/volumes/", pool)) 30 | names = append(names, fields[len(fields)-1]) 31 | } 32 | 33 | return names, nil 34 | } 35 | 36 | // GetStoragePoolVolumes returns a list of StorageVolume entries for the provided pool 37 | func (r *ProtocolLXD) GetStoragePoolVolumes(pool string) ([]api.StorageVolume, error) { 38 | volumes := []api.StorageVolume{} 39 | 40 | // Fetch the raw value 41 | _, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?recursion=1", pool), nil, "", &volumes) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return volumes, nil 47 | } 48 | 49 | // GetStoragePoolVolume returns a StorageVolume entry for the provided pool and volume name 50 | func (r *ProtocolLXD) GetStoragePoolVolume(pool string, volType string, name string) (*api.StorageVolume, string, error) { 51 | volume := api.StorageVolume{} 52 | 53 | // Fetch the raw value 54 | etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", pool, volType, name), nil, "", &volume) 55 | if err != nil { 56 | return nil, "", err 57 | } 58 | 59 | return &volume, etag, nil 60 | } 61 | 62 | // CreateStoragePoolVolume defines a new storage volume 63 | func (r *ProtocolLXD) CreateStoragePoolVolume(pool string, volume api.StorageVolumesPost) error { 64 | // Send the request 65 | _, _, err := r.query("POST", fmt.Sprintf("/storage-pools/%s/volumes/%s", pool, volume.Type), volume, "") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // UpdateStoragePoolVolume updates the volume to match the provided StoragePoolVolume struct 74 | func (r *ProtocolLXD) UpdateStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePut, ETag string) error { 75 | // Send the request 76 | _, _, err := r.query("PUT", fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", pool, volType, name), volume, ETag) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // DeleteStoragePoolVolume deletes a storage pool 85 | func (r *ProtocolLXD) DeleteStoragePoolVolume(pool string, volType string, name string) error { 86 | // Send the request 87 | _, _, err := r.query("DELETE", fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", pool, volType, name), nil, "") 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /lxc/delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/lxc/lxd" 10 | "github.com/lxc/lxd/shared" 11 | "github.com/lxc/lxd/shared/api" 12 | "github.com/lxc/lxd/shared/gnuflag" 13 | "github.com/lxc/lxd/shared/i18n" 14 | ) 15 | 16 | type deleteCmd struct { 17 | force bool 18 | interactive bool 19 | } 20 | 21 | func (c *deleteCmd) showByDefault() bool { 22 | return true 23 | } 24 | 25 | func (c *deleteCmd) usage() string { 26 | return i18n.G( 27 | `Usage: lxc delete [:][/] [[:][/]...] 28 | 29 | Delete containers and snapshots.`) 30 | } 31 | 32 | func (c *deleteCmd) flags() { 33 | gnuflag.BoolVar(&c.force, "f", false, i18n.G("Force the removal of stopped containers")) 34 | gnuflag.BoolVar(&c.force, "force", false, i18n.G("Force the removal of stopped containers")) 35 | gnuflag.BoolVar(&c.interactive, "i", false, i18n.G("Require user confirmation")) 36 | gnuflag.BoolVar(&c.interactive, "interactive", false, i18n.G("Require user confirmation")) 37 | } 38 | 39 | func (c *deleteCmd) promptDelete(name string) error { 40 | reader := bufio.NewReader(os.Stdin) 41 | fmt.Printf(i18n.G("Remove %s (yes/no): "), name) 42 | input, _ := reader.ReadString('\n') 43 | input = strings.TrimSuffix(input, "\n") 44 | if !shared.StringInSlice(strings.ToLower(input), []string{i18n.G("yes")}) { 45 | return fmt.Errorf(i18n.G("User aborted delete operation.")) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (c *deleteCmd) doDelete(d *lxd.Client, name string) error { 52 | resp, err := d.Delete(name) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return d.WaitForSuccess(resp.Operation) 58 | } 59 | 60 | func (c *deleteCmd) run(config *lxd.Config, args []string) error { 61 | if len(args) == 0 { 62 | return errArgs 63 | } 64 | 65 | for _, nameArg := range args { 66 | remote, name := config.ParseRemoteAndContainer(nameArg) 67 | 68 | d, err := lxd.NewClient(config, remote) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if c.interactive { 74 | err := c.promptDelete(name) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | if shared.IsSnapshot(name) { 81 | return c.doDelete(d, name) 82 | } 83 | 84 | ct, err := d.ContainerInfo(name) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | if ct.StatusCode != 0 && ct.StatusCode != api.Stopped { 90 | if !c.force { 91 | return fmt.Errorf(i18n.G("The container is currently running, stop it first or pass --force.")) 92 | } 93 | 94 | resp, err := d.Action(name, shared.Stop, -1, true, false) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | op, err := d.WaitFor(resp.Operation) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if op.StatusCode == api.Failure { 105 | return fmt.Errorf(i18n.G("Stopping container failed!")) 106 | } 107 | 108 | if ct.Ephemeral == true { 109 | return nil 110 | } 111 | } 112 | 113 | if err := c.doDelete(d, name); err != nil { 114 | return err 115 | } 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /lxd/storage_volumes_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lxc/lxd/shared" 8 | "github.com/lxc/lxd/shared/api" 9 | ) 10 | 11 | var storageVolumeConfigKeys = map[string]func(value string) error{ 12 | "block.mount_options": shared.IsAny, 13 | "block.filesystem": func(value string) error { 14 | return shared.IsOneOf(value, []string{"ext4", "xfs"}) 15 | }, 16 | "size": func(value string) error { 17 | if value == "" { 18 | return nil 19 | } 20 | 21 | _, err := shared.ParseByteSizeString(value) 22 | return err 23 | }, 24 | "zfs.use_refquota": shared.IsBool, 25 | "zfs.remove_snapshots": shared.IsBool, 26 | } 27 | 28 | func storageVolumeValidateConfig(name string, config map[string]string, parentPool *api.StoragePool) error { 29 | for key, val := range config { 30 | // User keys are not validated. 31 | if strings.HasPrefix(key, "user.") { 32 | continue 33 | } 34 | 35 | // Validate storage volume config keys. 36 | validator, ok := storageVolumeConfigKeys[key] 37 | if !ok { 38 | return fmt.Errorf("Invalid storage volume configuration key: %s", key) 39 | } 40 | 41 | err := validator(val) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if parentPool.Driver != "zfs" || parentPool.Driver == "dir" { 47 | if config["zfs.use_refquota"] != "" { 48 | return fmt.Errorf("the key volume.zfs.use_refquota cannot be used with non zfs storage volumes") 49 | } 50 | 51 | if config["zfs.remove_snapshots"] != "" { 52 | return fmt.Errorf("the key volume.zfs.remove_snapshots cannot be used with non zfs storage volumes") 53 | } 54 | } 55 | 56 | if parentPool.Driver == "dir" { 57 | if config["block.mount_options"] != "" { 58 | return fmt.Errorf("the key block.mount_options cannot be used with dir storage volumes") 59 | } 60 | 61 | if config["block.filesystem"] != "" { 62 | return fmt.Errorf("the key block.filesystem cannot be used with dir storage volumes") 63 | } 64 | 65 | if config["size"] != "" { 66 | return fmt.Errorf("the key size cannot be used with dir storage volumes") 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func storageVolumeFillDefault(name string, config map[string]string, parentPool *api.StoragePool) error { 75 | if parentPool.Driver == "dir" { 76 | config["size"] = "" 77 | } else if parentPool.Driver == "lvm" { 78 | if config["block.filesystem"] == "" { 79 | config["block.filesystem"] = parentPool.Config["volume.block.filesystem"] 80 | } 81 | if config["block.filesystem"] == "" { 82 | // Unchangeable volume property: Set unconditionally. 83 | config["block.filesystem"] = "ext4" 84 | } 85 | 86 | if config["size"] == "0" || config["size"] == "" { 87 | config["size"] = parentPool.Config["volume.size"] 88 | } 89 | 90 | if config["size"] == "0" || config["size"] == "" { 91 | config["size"] = "10GB" 92 | } 93 | } else { 94 | if config["size"] != "" { 95 | _, err := shared.ParseByteSizeString(config["size"]) 96 | if err != nil { 97 | return err 98 | } 99 | config["size"] = "10GB" 100 | } 101 | 102 | } 103 | 104 | return nil 105 | } 106 | --------------------------------------------------------------------------------