github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/container/lxd/lxd.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build go1.3
     5  
     6  package lxd
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/names"
    14  	"github.com/lxc/lxd"
    15  
    16  	"github.com/juju/juju/cloudconfig/containerinit"
    17  	"github.com/juju/juju/cloudconfig/instancecfg"
    18  	"github.com/juju/juju/container"
    19  	"github.com/juju/juju/instance"
    20  	"github.com/juju/juju/network"
    21  	"github.com/juju/juju/status"
    22  	"github.com/juju/juju/tools/lxdclient"
    23  )
    24  
    25  var (
    26  	logger = loggo.GetLogger("juju.container.lxd")
    27  )
    28  
    29  // XXX: should we allow managing containers on other hosts? this is
    30  // functionality LXD gives us and from discussion juju would use eventually for
    31  // the local provider, so the APIs probably need to be changed to pass extra
    32  // args around. I'm punting for now.
    33  type containerManager struct {
    34  	name string
    35  	// A cached client.
    36  	client *lxdclient.Client
    37  	// Custom network profile
    38  	networkProfile string
    39  }
    40  
    41  // containerManager implements container.Manager.
    42  var _ container.Manager = (*containerManager)(nil)
    43  
    44  func ConnectLocal(namespace string) (*lxdclient.Client, error) {
    45  	cfg := lxdclient.Config{
    46  		Namespace: namespace,
    47  		Remote:    lxdclient.Local,
    48  	}
    49  
    50  	cfg, err := cfg.WithDefaults()
    51  	if err != nil {
    52  		return nil, errors.Trace(err)
    53  	}
    54  
    55  	client, err := lxdclient.Connect(cfg)
    56  	if err != nil {
    57  		return nil, errors.Trace(err)
    58  	}
    59  
    60  	return client, nil
    61  }
    62  
    63  // NewContainerManager creates the entity that knows how to create and manage
    64  // LXD containers.
    65  // TODO(jam): This needs to grow support for things like LXC's ImageURLGetter
    66  // functionality.
    67  func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) {
    68  	name := conf.PopValue(container.ConfigName)
    69  	if name == "" {
    70  		return nil, errors.Errorf("name is required")
    71  	}
    72  
    73  	conf.WarnAboutUnused()
    74  	return &containerManager{name: name}, nil
    75  }
    76  
    77  func (manager *containerManager) CreateContainer(
    78  	instanceConfig *instancecfg.InstanceConfig,
    79  	series string,
    80  	networkConfig *container.NetworkConfig,
    81  	storageConfig *container.StorageConfig,
    82  	callback container.StatusCallback,
    83  ) (inst instance.Instance, _ *instance.HardwareCharacteristics, err error) {
    84  
    85  	defer func() {
    86  		if err != nil {
    87  			manager.deleteNetworkProfile()
    88  			callback(status.StatusProvisioningError, fmt.Sprintf("Creating container: %v", err), nil)
    89  		}
    90  	}()
    91  
    92  	if manager.client == nil {
    93  		manager.client, err = ConnectLocal(manager.name)
    94  		if err != nil {
    95  			err = errors.Annotatef(err, "failed to connect to local LXD")
    96  			return
    97  		}
    98  	}
    99  
   100  	err = manager.client.EnsureImageExists(series,
   101  		lxdclient.DefaultImageSources,
   102  		func(progress string) {
   103  			callback(status.StatusProvisioning, progress, nil)
   104  		})
   105  	if err != nil {
   106  		err = errors.Annotatef(err, "failed to ensure LXD image")
   107  		return
   108  	}
   109  
   110  	name := names.NewMachineTag(instanceConfig.MachineId).String()
   111  	if manager.name != "" {
   112  		name = fmt.Sprintf("%s-%s", manager.name, name)
   113  	}
   114  
   115  	userData, err := containerinit.CloudInitUserData(instanceConfig, networkConfig)
   116  	if err != nil {
   117  		return
   118  	}
   119  
   120  	metadata := map[string]string{
   121  		lxdclient.UserdataKey: string(userData),
   122  		// An extra piece of info to let people figure out where this
   123  		// thing came from.
   124  		"user.juju-environment": manager.name,
   125  
   126  		// Make sure these come back up on host reboot.
   127  		"boot.autostart": "true",
   128  	}
   129  
   130  	networkProfile := fmt.Sprintf("%s-network", name)
   131  
   132  	if len(networkConfig.Interfaces) > 0 || networkConfig.Device != "" {
   133  		if err = createNetworkProfile(manager.client, networkProfile); err != nil {
   134  			return
   135  		}
   136  
   137  		manager.networkProfile = networkProfile
   138  		if len(networkConfig.Interfaces) > 0 {
   139  			err = networkProfileAddMultipleInterfaces(manager.client, networkProfile, networkConfig.Interfaces)
   140  		} else {
   141  			err = networkProfileAddSingleInterface(manager.client, networkProfile, networkConfig.Device, networkConfig.MTU)
   142  		}
   143  		if err != nil {
   144  			return
   145  		}
   146  	} else {
   147  		networkProfile = "default"
   148  	}
   149  
   150  	spec := lxdclient.InstanceSpec{
   151  		Name:     name,
   152  		Image:    manager.client.ImageNameForSeries(series),
   153  		Metadata: metadata,
   154  		Profiles: []string{
   155  			networkProfile,
   156  		},
   157  	}
   158  
   159  	logger.Infof("starting instance %q (image %q)...", spec.Name, spec.Image)
   160  	callback(status.StatusProvisioning, "Starting container", nil)
   161  	_, err = manager.client.AddInstance(spec)
   162  	if err != nil {
   163  		manager.client.ProfileDelete(networkProfile)
   164  		return
   165  	}
   166  
   167  	callback(status.StatusRunning, "Container started", nil)
   168  	inst = &lxdInstance{name, manager.client}
   169  	return
   170  }
   171  
   172  func (manager *containerManager) DestroyContainer(id instance.Id) error {
   173  	if manager.client == nil {
   174  		var err error
   175  		manager.client, err = ConnectLocal(manager.name)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  	manager.deleteNetworkProfile()
   181  	return errors.Trace(manager.client.RemoveInstances(manager.name, string(id)))
   182  }
   183  
   184  func (manager *containerManager) ListContainers() (result []instance.Instance, err error) {
   185  	result = []instance.Instance{}
   186  	if manager.client == nil {
   187  		manager.client, err = ConnectLocal(manager.name)
   188  		if err != nil {
   189  			return
   190  		}
   191  	}
   192  
   193  	lxdInstances, err := manager.client.Instances(manager.name)
   194  	if err != nil {
   195  		return
   196  	}
   197  
   198  	for _, i := range lxdInstances {
   199  		result = append(result, &lxdInstance{i.Name, manager.client})
   200  	}
   201  
   202  	return
   203  }
   204  
   205  func (manager *containerManager) IsInitialized() bool {
   206  	if manager.client != nil {
   207  		return true
   208  	}
   209  
   210  	// NewClient does a roundtrip to the server to make sure it understands
   211  	// the versions, so all we need to do is connect above and we're done.
   212  	var err error
   213  	manager.client, err = ConnectLocal(manager.name)
   214  	return err == nil
   215  }
   216  
   217  // HasLXDSupport returns false when this juju binary was not built with LXD
   218  // support (i.e. it was built on a golang version < 1.2
   219  func HasLXDSupport() bool {
   220  	return true
   221  }
   222  
   223  func nicProperties(parentDevice, deviceName, hwAddr string, mtu int) ([]string, error) {
   224  	var props = []string{"nictype=bridged"}
   225  
   226  	if parentDevice == "" {
   227  		return nil, errors.Errorf("invalid parent device")
   228  	} else {
   229  		props = append(props, fmt.Sprintf("parent=%v", parentDevice))
   230  	}
   231  
   232  	if deviceName == "" {
   233  		return nil, errors.Errorf("invalid device name")
   234  	} else {
   235  		props = append(props, fmt.Sprintf("name=%v", deviceName))
   236  	}
   237  
   238  	if hwAddr != "" {
   239  		props = append(props, fmt.Sprintf("hwaddr=%v", hwAddr))
   240  	}
   241  
   242  	if mtu > 0 {
   243  		props = append(props, fmt.Sprintf("mtu=%v", mtu))
   244  	}
   245  
   246  	return props, nil
   247  }
   248  
   249  func addNetworkDeviceToProfile(client *lxdclient.Client, profile, parentDevice, deviceName, hwAddr string, mtu int) (*lxd.Response, error) {
   250  	props, err := nicProperties(parentDevice, deviceName, hwAddr, mtu)
   251  	if err != nil {
   252  		return nil, errors.Trace(err)
   253  	}
   254  	logger.Infof("adding nic device %q with properties %+v to profile %q", deviceName, props, profile)
   255  	return client.ProfileDeviceAdd(profile, deviceName, "nic", props)
   256  }
   257  
   258  func networkProfileAddSingleInterface(client *lxdclient.Client, profile, deviceName string, mtu int) error {
   259  	_, err := addNetworkDeviceToProfile(client, profile, deviceName, "eth0", "", mtu)
   260  	return errors.Trace(err)
   261  }
   262  
   263  func networkProfileAddMultipleInterfaces(client *lxdclient.Client, profile string, interfaces []network.InterfaceInfo) error {
   264  	for _, v := range interfaces {
   265  		if v.InterfaceType == network.LoopbackInterface {
   266  			continue
   267  		}
   268  
   269  		if v.InterfaceType != network.EthernetInterface {
   270  			return errors.Errorf("interface type %q not supported", v.InterfaceType)
   271  		}
   272  
   273  		_, err := addNetworkDeviceToProfile(client, profile, v.ParentInterfaceName, v.InterfaceName, v.MACAddress, v.MTU)
   274  
   275  		if err != nil {
   276  			return errors.Trace(err)
   277  		}
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  func createNetworkProfile(client *lxdclient.Client, profile string) error {
   284  	found, err := client.HasProfile(profile)
   285  
   286  	if err != nil {
   287  		return errors.Trace(err)
   288  	}
   289  
   290  	if found {
   291  		logger.Infof("deleting existing container profile %q", profile)
   292  		if err := client.ProfileDelete(profile); err != nil {
   293  			return errors.Trace(err)
   294  		}
   295  	}
   296  
   297  	err = client.CreateProfile(profile, nil)
   298  
   299  	if err == nil {
   300  		logger.Infof("created new network container profile %q", profile)
   301  	}
   302  
   303  	return errors.Trace(err)
   304  }
   305  
   306  func (manager *containerManager) deleteNetworkProfile() {
   307  	if manager.client != nil && manager.networkProfile != "" {
   308  		logger.Infof("deleting container network profile %q", manager.networkProfile)
   309  		if err := manager.client.ProfileDelete(manager.networkProfile); err != nil {
   310  			logger.Warningf("discarding profile delete error: %v", err)
   311  		}
   312  		manager.networkProfile = ""
   313  	}
   314  }