Exemple #1
0
func (k *daemonConfigKey) Validate(d *Daemon, value string) error {
	// No need to validate when unsetting
	if value == "" {
		return nil
	}

	// Validate booleans
	if k.valueType == "bool" && !shared.StringInSlice(strings.ToLower(value), []string{"true", "false", "1", "0", "yes", "no", "on", "off"}) {
		return fmt.Errorf("Invalid value for a boolean: %s", value)
	}

	// Validate integers
	if k.valueType == "int" {
		_, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			return err
		}
	}

	// Check against valid values
	if k.validValues != nil && !shared.StringInSlice(value, k.validValues) {
		return fmt.Errorf("Invalid value, only the following values are allowed: %s", k.validValues)
	}

	// Run external validation function
	if k.validator != nil {
		err := k.validator(d, k.name(), value)
		if err != nil {
			return err
		}
	}

	return nil
}
Exemple #2
0
func (s *storageZfs) zfsGetPoolUsers() ([]string, error) {
	subvols, err := s.zfsListSubvolumes("")
	if err != nil {
		return []string{}, err
	}

	exceptions := []string{
		"containers",
		"images",
		"snapshots",
		"deleted",
		"deleted/containers",
		"deleted/images"}

	users := []string{}
	for _, subvol := range subvols {
		path := strings.Split(subvol, "/")

		// Only care about plausible LXD paths
		if !shared.StringInSlice(path[0], exceptions) {
			continue
		}

		// Ignore empty paths
		if shared.StringInSlice(subvol, exceptions) {
			continue
		}

		users = append(users, subvol)
	}

	return users, nil
}
func cmdGenerateDNS(c *lxd.Client, args []string) error {
	// A path must be provided
	if len(args) < 1 {
		return fmt.Errorf("A format must be provided (samba4 or bind9).")
	}

	format := args[0]
	if !shared.StringInSlice(format, []string{"samba4", "bind9"}) {
		return fmt.Errorf("Invalid format, supported values are samba4 or bind9")
	}

	// Load the simulation
	routers, err := importFromLXD(c)
	if err != nil {
		return err
	}

	for _, r := range routers {
		for _, d := range r.DNS {
			if format == "bind9" {
				fmt.Println(d)
			} else {
				fields := strings.Fields(d)
				domain := strings.Split(fields[0], ".")

				fmt.Printf("samba-tool dns add localhost %s %s %s %s\n", domain[len(domain)-1], fields[0], fields[2], fields[3])
			}
		}
	}

	return nil
}
Exemple #4
0
func (s *storageZfs) zfsGetPoolUsers() ([]string, error) {
	subvols, err := s.zfsListSubvolumes("")
	if err != nil {
		return []string{}, err
	}

	exceptions := []string{
		"containers",
		"images",
		"snapshots",
		"deleted",
		"deleted/containers",
		"deleted/images"}

	users := []string{}
	for _, subvol := range subvols {
		if shared.StringInSlice(subvol, exceptions) {
			continue
		}

		users = append(users, subvol)
	}

	return users, nil
}
Exemple #5
0
func imageValidSecret(fingerprint string, secret string) bool {
	for _, op := range operations {
		if op.resources == nil {
			continue
		}

		opImages, ok := op.resources["images"]
		if !ok {
			continue
		}

		if !shared.StringInSlice(fingerprint, opImages) {
			continue
		}

		opSecret, ok := op.metadata["secret"]
		if !ok {
			continue
		}

		if opSecret == secret {
			// Token is single-use, so cancel it now
			op.Cancel()
			return true
		}
	}

	return false
}
Exemple #6
0
// The handler for the post operation.
func profilePost(d *Daemon, r *http.Request) Response {
	name := mux.Vars(r)["name"]

	req := profilesPostReq{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Sanity checks
	if req.Name == "" {
		return BadRequest(fmt.Errorf("No name provided"))
	}

	// Check that the name isn't already in use
	id, _, _ := dbProfileGet(d.db, req.Name)
	if id > 0 {
		return Conflict
	}

	if strings.Contains(req.Name, "/") {
		return BadRequest(fmt.Errorf("Profile names may not contain slashes"))
	}

	if shared.StringInSlice(req.Name, []string{".", ".."}) {
		return BadRequest(fmt.Errorf("Invalid profile name '%s'", req.Name))
	}

	err := dbProfileUpdate(d.db, name, req.Name)
	if err != nil {
		return InternalError(err)
	}

	return SyncResponseLocation(true, nil, fmt.Sprintf("/%s/profiles/%s", shared.APIVersion, req.Name))
}
Exemple #7
0
func run(args []string) error {
	// Parse command line
	gnuflag.Parse(true)

	if len(os.Args) == 1 || !shared.StringInSlice(os.Args[1], []string{"spawn", "delete"}) {
		fmt.Printf("Usage: %s spawn [--count=COUNT] [--image=IMAGE] [--privileged=BOOL] [--parallel=COUNT]\n", os.Args[0])
		fmt.Printf("       %s delete [--parallel=COUNT]\n\n", os.Args[0])
		gnuflag.Usage()
		fmt.Printf("\n")
		return fmt.Errorf("An action (spawn or delete) must be passed.")
	}

	// Connect to LXD
	c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
	if err != nil {
		return err
	}

	switch os.Args[1] {
	case "spawn":
		return spawnContainers(c, *argCount, *argImage, *argPrivileged)
	case "delete":
		return deleteContainers(c)
	}

	return nil
}
Exemple #8
0
func compressFile(path string, compress string) (string, error) {
	reproducible := []string{"gzip"}

	args := []string{path, "-c"}
	if shared.StringInSlice(compress, reproducible) {
		args = append(args, "-n")
	}

	cmd := exec.Command(compress, args...)

	outfile, err := os.Create(path + ".compressed")
	if err != nil {
		return "", err
	}

	defer outfile.Close()
	cmd.Stdout = outfile

	err = cmd.Run()
	if err != nil {
		os.Remove(outfile.Name())
		return "", err
	}

	return outfile.Name(), nil
}
Exemple #9
0
func (c *profileCmd) doProfileRemove(client *lxd.Client, d string, p string) error {
	ct, err := client.ContainerInfo(d)
	if err != nil {
		return err
	}

	if !shared.StringInSlice(p, ct.Profiles) {
		return fmt.Errorf("Profile %s isn't currently applied to %s", p, d)
	}

	profiles := []string{}
	for _, profile := range ct.Profiles {
		if profile == p {
			continue
		}

		profiles = append(profiles, profile)
	}

	ct.Profiles = profiles

	err = client.UpdateContainerConfig(d, ct.Brief())
	if err != nil {
		return err
	}

	fmt.Printf(i18n.G("Profile %s removed from %s")+"\n", p, d)

	return err
}
Exemple #10
0
func containerValidDevices(devices shared.Devices) error {
	// Empty device list
	if devices == nil {
		return nil
	}

	// Check each device individually
	for _, m := range devices {
		for k, _ := range m {
			if !containerValidDeviceConfigKey(m["type"], k) {
				return fmt.Errorf("Invalid device configuration key for %s: %s", m["type"], k)
			}
		}

		if m["type"] == "nic" {
			if m["nictype"] == "" {
				return fmt.Errorf("Missing nic type")
			}

			if !shared.StringInSlice(m["nictype"], []string{"bridged", "physical", "p2p", "macvlan"}) {
				return fmt.Errorf("Bad nic type: %s", m["nictype"])
			}

			if shared.StringInSlice(m["nictype"], []string{"bridged", "physical", "macvlan"}) && m["parent"] == "" {
				return fmt.Errorf("Missing parent for %s type nic.", m["nictype"])
			}
		} else if m["type"] == "disk" {
			if m["path"] == "" {
				return fmt.Errorf("Disk entry is missing the required \"path\" property.")
			}

			if m["source"] == "" && m["path"] != "/" {
				return fmt.Errorf("Disk entry is missing the required \"source\" property.")
			}
		} else if shared.StringInSlice(m["type"], []string{"unix-char", "unix-block"}) {
			if m["path"] == "" {
				return fmt.Errorf("Unix device entry is missing the required \"path\" property.")
			}
		} else if m["type"] == "none" {
			continue
		} else {
			return fmt.Errorf("Invalid device type: %s", m["type"])
		}
	}

	return nil
}
Exemple #11
0
func (s *storageZfs) zfsClone(source string, name string, dest string, dotZfs bool) error {
	var mountpoint string

	mountpoint = shared.VarPath(dest)
	if dotZfs {
		mountpoint += ".zfs"
	}

	output, err := exec.Command(
		"zfs",
		"clone",
		"-p",
		"-o", fmt.Sprintf("mountpoint=%s", mountpoint),
		fmt.Sprintf("%s/%s@%s", s.zfsPool, source, name),
		fmt.Sprintf("%s/%s", s.zfsPool, dest)).CombinedOutput()
	if err != nil {
		s.log.Error("zfs clone failed", log.Ctx{"output": string(output)})
		return fmt.Errorf("Failed to clone the filesystem: %s", output)
	}

	subvols, err := s.zfsListSubvolumes(source)
	if err != nil {
		return err
	}

	for _, sub := range subvols {
		snaps, err := s.zfsListSnapshots(sub)
		if err != nil {
			return err
		}

		if !shared.StringInSlice(name, snaps) {
			continue
		}

		destSubvol := dest + strings.TrimPrefix(sub, source)
		mountpoint = shared.VarPath(destSubvol)
		if dotZfs {
			mountpoint += ".zfs"
		}

		output, err := exec.Command(
			"zfs",
			"clone",
			"-p",
			"-o", fmt.Sprintf("mountpoint=%s", mountpoint),
			fmt.Sprintf("%s/%s@%s", s.zfsPool, sub, name),
			fmt.Sprintf("%s/%s", s.zfsPool, destSubvol)).CombinedOutput()
		if err != nil {
			s.log.Error("zfs clone failed", log.Ctx{"output": string(output)})
			return fmt.Errorf("Failed to clone the sub-volume: %s", output)
		}
	}

	return nil
}
Exemple #12
0
func (c *deleteCmd) promptDelete(name string) error {
	reader := bufio.NewReader(os.Stdin)
	fmt.Printf(i18n.G("Remove %s (yes/no): "), name)
	input, _ := reader.ReadString('\n')
	input = strings.TrimSuffix(input, "\n")
	if !shared.StringInSlice(strings.ToLower(input), []string{i18n.G("yes")}) {
		return fmt.Errorf(i18n.G("User aborted delete operation."))
	}

	return nil
}
Exemple #13
0
func containersPost(d *Daemon, r *http.Request) Response {
	shared.LogDebugf("Responding to container create")

	req := containerPostReq{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	if req.Name == "" {
		cs, err := dbContainersList(d.db, cTypeRegular)
		if err != nil {
			return InternalError(err)
		}

		i := 0
		for {
			i++
			req.Name = strings.ToLower(petname.Generate(2, "-"))
			if !shared.StringInSlice(req.Name, cs) {
				break
			}

			if i > 100 {
				return InternalError(fmt.Errorf("couldn't generate a new unique name after 100 tries"))
			}
		}
		shared.LogDebugf("No name provided, creating %s", req.Name)
	}

	if req.Devices == nil {
		req.Devices = shared.Devices{}
	}

	if req.Config == nil {
		req.Config = map[string]string{}
	}

	if strings.Contains(req.Name, shared.SnapshotDelimiter) {
		return BadRequest(fmt.Errorf("Invalid container name: '%s' is reserved for snapshots", shared.SnapshotDelimiter))
	}

	switch req.Source.Type {
	case "image":
		return createFromImage(d, &req)
	case "none":
		return createFromNone(d, &req)
	case "migration":
		return createFromMigration(d, &req)
	case "copy":
		return createFromCopy(d, &req)
	default:
		return BadRequest(fmt.Errorf("unknown source type %s", req.Source.Type))
	}
}
Exemple #14
0
func (c *Client) Action(name string, action shared.ContainerAction, timeout int, force bool, stateful bool) (*Response, error) {
	body := shared.Jmap{
		"action":  action,
		"timeout": timeout,
		"force":   force}

	if shared.StringInSlice(string(action), []string{"start", "stop"}) {
		body["stateful"] = stateful
	}

	return c.put(fmt.Sprintf("containers/%s/state", name), body, Async)
}
Exemple #15
0
func (c *networkCmd) doNetworkList(config *lxd.Config, args []string) error {
	var remote string
	if len(args) > 1 {
		var name string
		remote, name = config.ParseRemoteAndContainer(args[1])
		if name != "" {
			return fmt.Errorf(i18n.G("Cannot provide container name to list"))
		}
	} else {
		remote = config.DefaultRemote
	}

	client, err := lxd.NewClient(config, remote)
	if err != nil {
		return err
	}

	networks, err := client.ListNetworks()
	if err != nil {
		return err
	}

	data := [][]string{}
	for _, network := range networks {
		if shared.StringInSlice(network.Type, []string{"loopback", "unknown"}) {
			continue
		}

		strManaged := i18n.G("NO")
		if network.Managed {
			strManaged = i18n.G("YES")
		}

		strUsedBy := fmt.Sprintf("%d", len(network.UsedBy))
		data = append(data, []string{network.Name, network.Type, strManaged, strUsedBy})
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetAutoWrapText(false)
	table.SetAlignment(tablewriter.ALIGN_LEFT)
	table.SetRowLine(true)
	table.SetHeader([]string{
		i18n.G("NAME"),
		i18n.G("TYPE"),
		i18n.G("MANAGED"),
		i18n.G("USED BY")})
	sort.Sort(byName(data))
	table.AppendBulk(data)
	table.Render()

	return nil
}
Exemple #16
0
func networkInterfaces(routers Routers) ([]string, []string, error) {
	vethInterfaces := []string{}
	bridgeInterfaces := []string{}

	for _, r := range routers {
		for _, p := range r.Peers {
			if strings.HasPrefix(p.Interface, "v") {
				veth := strings.TrimSuffix(strings.TrimSuffix(p.Interface, "-1"), "-2")

				if !shared.StringInSlice(veth, vethInterfaces) {
					vethInterfaces = append(vethInterfaces, veth)
				}
			} else if strings.HasPrefix(p.Interface, "br") {
				if !shared.StringInSlice(p.Interface, bridgeInterfaces) {
					bridgeInterfaces = append(bridgeInterfaces, p.Interface)
				}
			} else {
				return nil, nil, fmt.Errorf("Invalid interface name: %s", p.Interface)
			}
		}
	}

	return vethInterfaces, bridgeInterfaces, nil
}
Exemple #17
0
func networkGetTunnels(config map[string]string) []string {
	tunnels := []string{}

	for k, _ := range config {
		if !strings.HasPrefix(k, "tunnel.") {
			continue
		}

		fields := strings.Split(k, ".")
		if !shared.StringInSlice(fields[1], tunnels) {
			tunnels = append(tunnels, fields[1])
		}
	}

	return tunnels
}
Exemple #18
0
// Connect opens an API connection to LXD and returns a high-level
// Client wrapper around that connection.
func Connect(cfg Config, verifyBridgeConfig bool) (*Client, error) {
	if err := cfg.Validate(); err != nil {
		return nil, errors.Trace(err)
	}

	remoteID := cfg.Remote.ID()

	raw, err := newRawClient(cfg.Remote)
	if err != nil {
		return nil, errors.Trace(err)
	}

	networkAPISupported := false
	if cfg.Remote.Protocol != SimplestreamsProtocol {
		status, err := raw.ServerStatus()
		if err != nil {
			return nil, errors.Trace(err)
		}

		if lxdshared.StringInSlice("network", status.APIExtensions) {
			networkAPISupported = true
		}
	}

	var bridgeName string
	if remoteID == remoteIDForLocal && verifyBridgeConfig {
		// If this is the LXD provider on the localhost, let's do an extra check to
		// make sure the default profile has a correctly configured bridge, and
		// which one is it.
		bridgeName, err = verifyDefaultProfileBridgeConfig(raw, networkAPISupported)
		if err != nil {
			return nil, errors.Trace(err)
		}
	}

	conn := &Client{
		serverConfigClient:       &serverConfigClient{raw},
		certClient:               &certClient{raw},
		profileClient:            &profileClient{raw},
		instanceClient:           &instanceClient{raw, remoteID},
		imageClient:              &imageClient{raw, connectToRaw},
		networkClient:            &networkClient{raw, networkAPISupported},
		baseURL:                  raw.BaseURL,
		defaultProfileBridgeName: bridgeName,
	}
	return conn, nil
}
Exemple #19
0
func (c *deleteCmd) doDelete(d *lxd.Client, name string) error {
	if c.interactive {
		reader := bufio.NewReader(os.Stdin)
		fmt.Printf(i18n.G("Remove %s (yes/no): "), name)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSuffix(input, "\n")
		if !shared.StringInSlice(strings.ToLower(input), []string{i18n.G("yes")}) {
			return fmt.Errorf(i18n.G("User aborted delete operation."))
		}
	}

	resp, err := d.Delete(name)
	if err != nil {
		return err
	}

	return d.WaitForSuccess(resp.Operation)
}
Exemple #20
0
func patchInvalidProfileNames(name string, d *Daemon) error {
	profiles, err := dbProfiles(d.db)
	if err != nil {
		return err
	}

	for _, profile := range profiles {
		if strings.Contains(profile, "/") || shared.StringInSlice(profile, []string{".", ".."}) {
			shared.LogInfo("Removing unreachable profile (invalid name)", log.Ctx{"name": profile})
			err := dbProfileDelete(d.db, profile)
			if err != nil {
				return err
			}
		}
	}

	return nil
}
Exemple #21
0
func networkPost(d *Daemon, r *http.Request) Response {
	name := mux.Vars(r)["name"]
	req := shared.NetworkConfig{}

	// Parse the request
	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		return BadRequest(err)
	}

	// Get the existing network
	n, err := networkLoadByName(d, name)
	if err != nil {
		return NotFound
	}

	// Sanity checks
	if req.Name == "" {
		return BadRequest(fmt.Errorf("No name provided"))
	}

	err = networkValidName(req.Name)
	if err != nil {
		return BadRequest(err)
	}

	// Check that the name isn't already in use
	networks, err := networkGetInterfaces(d)
	if err != nil {
		return InternalError(err)
	}

	if shared.StringInSlice(req.Name, networks) {
		return Conflict
	}

	// Rename it
	err = n.Rename(req.Name)
	if err != nil {
		return SmartError(err)
	}

	return SyncResponseLocation(true, nil, fmt.Sprintf("/%s/networks/%s", shared.APIVersion, req.Name))
}
Exemple #22
0
func profilesPost(d *Daemon, r *http.Request) Response {
	req := profilesPostReq{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Sanity checks
	if req.Name == "" {
		return BadRequest(fmt.Errorf("No name provided"))
	}

	_, profile, _ := dbProfileGet(d.db, req.Name)
	if profile != nil {
		return BadRequest(fmt.Errorf("The profile already exists"))
	}

	if strings.Contains(req.Name, "/") {
		return BadRequest(fmt.Errorf("Profile names may not contain slashes"))
	}

	if shared.StringInSlice(req.Name, []string{".", ".."}) {
		return BadRequest(fmt.Errorf("Invalid profile name '%s'", req.Name))
	}

	err := containerValidConfig(d, req.Config, true, false)
	if err != nil {
		return BadRequest(err)
	}

	err = containerValidDevices(req.Devices, true, false)
	if err != nil {
		return BadRequest(err)
	}

	// Update DB entry
	_, err = dbProfileCreate(d.db, req.Name, req.Description, req.Config, req.Devices)
	if err != nil {
		return InternalError(
			fmt.Errorf("Error inserting %s into database: %s", req.Name, err))
	}

	return SyncResponseLocation(true, nil, fmt.Sprintf("/%s/profiles/%s", shared.APIVersion, req.Name))
}
Exemple #23
0
func networkGetInterfaces(d *Daemon) ([]string, error) {
	networks, err := dbNetworks(d.db)
	if err != nil {
		return nil, err
	}

	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}

	for _, iface := range ifaces {
		if !shared.StringInSlice(iface.Name, networks) {
			networks = append(networks, iface.Name)
		}
	}

	return networks, nil
}
Exemple #24
0
func patchesApplyAll(d *Daemon) error {
	appliedPatches, err := dbPatches(d.db)
	if err != nil {
		return err
	}

	for _, patch := range patches {
		if shared.StringInSlice(patch.name, appliedPatches) {
			continue
		}

		err := patch.apply(d)
		if err != nil {
			return err
		}
	}

	return nil
}
Exemple #25
0
// Global functions
func storageZFSGetPoolUsers(d *Daemon) ([]string, error) {
	zfs := storageZfs{}

	err := zfs.initShared()
	if err != nil {
		return []string{}, err
	}

	zfsPool, err := d.ConfigValueGet("storage.zfs_pool_name")
	if err != nil {
		return []string{}, err
	}

	if zfsPool == "" {
		return []string{}, nil
	}

	zfs.zfsPool = zfsPool

	subvols, err := zfs.zfsListSubvolumes("")
	if err != nil {
		return []string{}, err
	}

	exceptions := []string{
		"containers",
		"images",
		"deleted",
		"deleted/containers",
		"deleted/images"}

	users := []string{}
	for _, subvol := range subvols {
		if shared.StringInSlice(subvol, exceptions) {
			continue
		}

		users = append(users, subvol)
	}

	return users, nil
}
Exemple #26
0
func isOnBridge(c container, bridge string) bool {
	for _, device := range c.ExpandedDevices() {
		if device["type"] != "nic" {
			continue
		}

		if !shared.StringInSlice(device["nictype"], []string{"bridged", "macvlan"}) {
			continue
		}

		if device["parent"] == "" {
			continue
		}

		if device["parent"] == bridge {
			return true
		}
	}

	return false
}
Exemple #27
0
func networkIsInUse(c container, name string) bool {
	for _, d := range c.ExpandedDevices() {
		if d["type"] != "nic" {
			continue
		}

		if !shared.StringInSlice(d["nictype"], []string{"bridged", "macvlan"}) {
			continue
		}

		if d["parent"] == "" {
			continue
		}

		if d["parent"] == name {
			return true
		}
	}

	return false
}
Exemple #28
0
func eventSend(eventType string, eventMessage interface{}) error {
	event := shared.Jmap{}
	event["type"] = eventType
	event["timestamp"] = time.Now()
	event["metadata"] = eventMessage

	body, err := json.Marshal(event)
	if err != nil {
		return err
	}

	eventsLock.Lock()
	listeners := eventListeners
	eventsLock.Unlock()

	for _, listener := range listeners {
		if !shared.StringInSlice(eventType, listener.messageTypes) {
			continue
		}

		go func(listener *eventListener, body []byte) {
			listener.msgLock.Lock()
			err = listener.connection.WriteMessage(websocket.TextMessage, body)
			listener.msgLock.Unlock()
			if err != nil {
				listener.connection.Close()
				listener.active <- false

				eventsLock.Lock()
				delete(eventListeners, listener.id)
				eventsLock.Unlock()

				shared.Debugf("Disconnected events listener: %s", listener.id)
			}
		}(listener, body)
	}

	return nil
}
Exemple #29
0
Fichier : list.go Projet : vahe/lxd
func (c *listCmd) IP6ColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
	if cInfo.IsActive() && cState != nil && cState.Network != nil {
		ipv6s := []string{}
		for netName, net := range cState.Network {
			if net.Type == "loopback" {
				continue
			}

			for _, addr := range net.Addresses {
				if shared.StringInSlice(addr.Scope, []string{"link", "local"}) {
					continue
				}

				if addr.Family == "inet6" {
					ipv6s = append(ipv6s, fmt.Sprintf("%s (%s)", addr.Address, netName))
				}
			}
		}
		return strings.Join(ipv6s, "\n")
	} else {
		return ""
	}
}
Exemple #30
0
func (s *storageZfs) zfsSnapshotRestore(path string, name string) error {
	output, err := tryExec(
		"zfs",
		"rollback",
		fmt.Sprintf("%s/%s@%s", s.zfsPool, path, name))
	if err != nil {
		s.log.Error("zfs rollback failed", log.Ctx{"output": string(output)})
		return fmt.Errorf("Failed to restore ZFS snapshot: %s", output)
	}

	subvols, err := s.zfsListSubvolumes(path)
	if err != nil {
		return err
	}

	for _, sub := range subvols {
		snaps, err := s.zfsListSnapshots(sub)
		if err != nil {
			return err
		}

		if !shared.StringInSlice(name, snaps) {
			continue
		}

		output, err := tryExec(
			"zfs",
			"rollback",
			fmt.Sprintf("%s/%s@%s", s.zfsPool, sub, name))
		if err != nil {
			s.log.Error("zfs rollback failed", log.Ctx{"output": string(output)})
			return fmt.Errorf("Failed to restore ZFS sub-volume snapshot: %s", output)
		}
	}

	return nil
}