From db065ff077164db6fedefeb17ad6b2dafa7d54ad Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 27 Jun 2016 14:41:46 +0200 Subject: Associate a cloud-init with a domain Allow the usage of a cloud-init ISO with a Domain. Signed-off-by: Flavio Castelli --- libvirt/cloudinit_def.go | 304 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 libvirt/cloudinit_def.go (limited to 'libvirt/cloudinit_def.go') diff --git a/libvirt/cloudinit_def.go b/libvirt/cloudinit_def.go new file mode 100644 index 00000000..a0572b5f --- /dev/null +++ b/libvirt/cloudinit_def.go @@ -0,0 +1,304 @@ +package libvirt + +import ( + "encoding/xml" + "fmt" + libvirt "github.com/dmacvicar/libvirt-go" + "github.com/hooklift/iso9660" + "gopkg.in/yaml.v2" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "time" +) + +// names of the files expected by cloud-init +const USERDATA string = "user-data" +const METADATA string = "meta-data" + +type defCloudInit struct { + Name string + PoolName string + Metadata struct { + LocalHostname string `yaml:"local-hostname"` + InstanceID string `yaml:"instance-id"` + } + UserData struct { + SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` + } +} + +// Creates a new cloudinit with the defaults +// the provider uses +func newCloudInitDef() defCloudInit { + ci := defCloudInit{} + ci.Metadata.InstanceID = fmt.Sprintf("created-at-%s", time.Now().String()) + + return ci +} + +// Create a ISO file based on the contents of the CloudInit instance and +// uploads it to the libVirt pool +// Returns a string holding the ID of the volume +func (ci *defCloudInit) CreateAndUpload(virConn *libvirt.VirConnection) (string, error) { + iso, err := ci.createISO() + if err != nil { + return "", err + } + + pool, err := virConn.LookupStoragePoolByName(ci.PoolName) + if err != nil { + return "", fmt.Errorf("can't find storage pool '%s'", ci.PoolName) + } + defer pool.Free() + + // Refresh the pool of the volume so that libvirt knows it is + // not longer in use. + WaitForSuccess("Error refreshing pool for volume", func() error { + return pool.Refresh(0) + }) + + volumeDef := newDefVolume() + volumeDef.Name = ci.Name + + // an existing image was given, this mean we can't choose size + img, err := newImage(iso) + if err != nil { + return "", err + } + defer func() { + // Remove the tmp directory holding the ISO + if err = os.RemoveAll(filepath.Dir(iso)); err != nil { + log.Printf("Error while removing tmp directory holding the ISO file: %s", err) + } + }() + + size, err := img.Size() + if err != nil { + return "", err + } + + volumeDef.Capacity.Unit = "B" + volumeDef.Capacity.Amount = size + + volumeDefXml, err := xml.Marshal(volumeDef) + if err != nil { + return "", fmt.Errorf("Error serializing libvirt volume: %s", err) + } + + // create the volume + volume, err := pool.StorageVolCreateXML(string(volumeDefXml), 0) + if err != nil { + return "", fmt.Errorf("Error creating libvirt volume: %s", err) + } + defer volume.Free() + + // upload ISO file + stream, err := libvirt.NewVirStream(virConn, 0) + if err != nil { + return "", err + } + defer stream.Close() + + volume.Upload(stream, 0, uint64(volumeDef.Capacity.Amount), 0) + err = img.WriteToStream(stream) + if err != nil { + return "", err + } + + key, err := volume.GetKey() + if err != nil { + return "", fmt.Errorf("Error retrieving volume key: %s", err) + } + + return key, nil +} + +// Create the ISO holding all the cloud-init data +// Returns a string with the full path to the ISO file +func (ci *defCloudInit) createISO() (string, error) { + log.Print("Creating new ISO") + tmpDir, err := ci.createFiles() + if err != nil { + return "", err + } + + isoDestination := filepath.Join(tmpDir, ci.Name) + cmd := exec.Command( + "genisoimage", + "-output", + isoDestination, + "-volid", + "cidata", + "-joliet", + "-rock", + filepath.Join(tmpDir, USERDATA), + filepath.Join(tmpDir, METADATA)) + + log.Print("About to execute cmd: %+v", cmd) + if err = cmd.Start(); err != nil { + return "", fmt.Errorf("Error while starting the creation of CloudInit's ISO image: %s", err) + } + if err = cmd.Wait(); err != nil { + return "", fmt.Errorf("Error while creating CloudInit's ISO image: %s", err) + } + log.Print("ISO created at %s", isoDestination) + + return isoDestination, nil +} + +// Dumps the userdata and the metadata into two dedicated yaml files. +// The files are created inside of a temporary directory +// Returns a string containing the name of the temporary directory and an error +// object +func (ci *defCloudInit) createFiles() (string, error) { + log.Print("Creating ISO contents") + tmpDir, err := ioutil.TempDir("", "cloudinit") + if err != nil { + return "", fmt.Errorf("Cannot create tmp directory for cloudinit ISO generation: %s", + err) + } + + // Create files required by ISO file + ud, err := yaml.Marshal(&ci.UserData) + if err != nil { + return "", fmt.Errorf("Error dumping cloudinit's user data: %s", err) + } + userdata := fmt.Sprintf("#cloud-config\n%s", string(ud)) + if err = ioutil.WriteFile( + filepath.Join(tmpDir, USERDATA), + []byte(userdata), + os.ModePerm); err != nil { + return "", fmt.Errorf("Error while writing user-data to file: %s", err) + } + + metadata, err := yaml.Marshal(&ci.Metadata) + if err != nil { + return "", fmt.Errorf("Error dumping cloudinit's meta data: %s", err) + } + if err = ioutil.WriteFile(filepath.Join(tmpDir, METADATA), metadata, os.ModePerm); err != nil { + return "", fmt.Errorf("Error while writing meta-data to file: %s", err) + } + + log.Print("ISO contents created") + + return tmpDir, nil +} + +func newCloudInitDefFromRemoteISO(virConn *libvirt.VirConnection, key string) (defCloudInit, error) { + ci := defCloudInit{} + + volume, err := virConn.LookupStorageVolByKey(key) + if err != nil { + return ci, fmt.Errorf("Can't retrieve volume %s", key) + } + defer volume.Free() + + ci.Name, err = volume.GetName() + if err != nil { + return ci, fmt.Errorf("Error retrieving volume name: %s", err) + } + + volPool, err := volume.LookupPoolByVolume() + if err != nil { + return ci, fmt.Errorf("Error retrieving pool for volume: %s", err) + } + defer volPool.Free() + + ci.PoolName, err = volPool.GetName() + if err != nil { + return ci, fmt.Errorf("Error retrieving pool name: %s", err) + } + + file, err := downloadISO(virConn, volume) + if file != nil { + defer os.Remove(file.Name()) + defer file.Close() + } + if err != nil { + return ci, err + } + + // read ISO contents + isoReader, err := iso9660.NewReader(file) + if err != nil { + return ci, fmt.Errorf("Error initializing ISO reader: %s", err) + } + + for { + f, err := isoReader.Next() + if err == io.EOF { + break + } + + if err != nil { + return ci, err + } + + log.Printf("ISO reader: processing file %s", f.Name()) + + //TODO: the iso9660 has a bug... + if f.Name() == "/user_dat." { + data, err := ioutil.ReadAll(f.Sys().(io.Reader)) + if err != nil { + return ci, fmt.Errorf("Error while reading %s: %s", USERDATA, err) + } + if err := yaml.Unmarshal(data, &ci.UserData); err != nil { + return ci, fmt.Errorf("Error while unmarshalling user-data: %s", err) + } + } + + //TODO: the iso9660 has a bug... + if f.Name() == "/meta_dat." { + data, err := ioutil.ReadAll(f.Sys().(io.Reader)) + if err != nil { + return ci, fmt.Errorf("Error while reading %s: %s", METADATA, err) + } + if err := yaml.Unmarshal(data, &ci.Metadata); err != nil { + return ci, fmt.Errorf("Error while unmarshalling user-data: %s", err) + } + } + } + + log.Printf("Read cloud-init from file: %+v", ci) + + return ci, nil +} + +// Downloads the ISO identified by `key` to a local tmp file. +// Returns a pointer to the ISO file. Note well: you have to close this file +// pointer when you are done. +func downloadISO(virConn *libvirt.VirConnection, volume libvirt.VirStorageVol) (*os.File, error) { + // get Volume info (required to get size later) + info, err := volume.GetInfo() + if err != nil { + return nil, fmt.Errorf("Error retrieving info for volume: %s", err) + } + + // create tmp file for the ISO + file, err := ioutil.TempFile("", "cloudinit") + if err != nil { + return nil, fmt.Errorf("Cannot create tmp file: %s", err) + } + + // download ISO file + stream, err := libvirt.NewVirStream(virConn, 0) + if err != nil { + return file, err + } + defer stream.Close() + + volume.Download(stream, 0, info.GetCapacityInBytes(), 0) + + n, err := io.Copy(file, stream) + if err != nil { + return file, fmt.Errorf("Error while copying remote volume to local disk: %s", err) + } + file.Seek(0, 0) + log.Printf("%d bytes downloaded", n) + + return file, nil +} -- cgit v1.2.3