Skip to content

Commit

Permalink
ConfigDrive utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
stbenjam committed Mar 1, 2019
1 parent 003f6f5 commit 8594c32
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 0 deletions.
120 changes: 120 additions & 0 deletions openstack/baremetal/v1/nodes/configdrive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package nodes

import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
)

// A ConfigDrive struct will be used to create a base64-encoded, gzipped ISO9660 image for use with Ironic.
type ConfigDrive struct {
UserData UserDataBuilder `json:"user_data"`
MetaData map[string]interface{} `json:"meta_data"`
NetworkData map[string]interface{} `json:"network_data"`
Version string `json:"-"`
}

// Interface to let us specify a raw string, or a map for the user data
type UserDataBuilder interface {
ToUserData() ([]byte, error)
}

type UserDataMap map[string]interface{}
type UserDataString string

// Converts a UserDataMap to JSON-string
func (data UserDataMap) ToUserData() ([]byte, error) {
return json.MarshalIndent(data, "", "\t")
}

func (data UserDataString) ToUserData() ([]byte, error) {
return []byte(data), nil
}

type ConfigDriveBuilder interface {
ToConfigDrive() (string, error)
}

// Writes out a ConfigDrive to a temporary directory, and returns the path
func (configDrive ConfigDrive) ToDirectory() (string, error) {
// Create a temporary directory for our config drive
directory, err := ioutil.TempDir("", "gophercloud")
if err != nil {
return "", err
}

// Build up the paths for OpenStack
var version string
if configDrive.Version == "" {
version = "latest"
} else {
version = configDrive.Version
}

path := filepath.FromSlash(directory + "/openstack/" + version)
if err := os.MkdirAll(path, 0755); err != nil {
return "", err
}

// Dump out user data
if configDrive.UserData != nil {
userDataPath := filepath.FromSlash(path + "/user_data")
data, err := configDrive.UserData.ToUserData()
if err != nil {
return "", err
}

if err := ioutil.WriteFile(userDataPath, data, 0644); err != nil {
return "", err
}
}

// Dump out meta data
if configDrive.MetaData != nil {
metaDataPath := filepath.FromSlash(path + "/meta_data.json")
data, err := json.Marshal(configDrive.MetaData)
if err != nil {
return "", err
}

if err := ioutil.WriteFile(metaDataPath, data, 0644); err != nil {
return "", err
}
}

// Dump out network data
if configDrive.NetworkData != nil {
networkDataPath := filepath.FromSlash(path + "/network_data.json")
data, err := json.Marshal(configDrive.NetworkData)
if err != nil {
return "", err
}

if err := ioutil.WriteFile(networkDataPath, data, 0644); err != nil {
return "", err
}
}

return directory, nil
}

// Writes out the ConfigDrive struct to a directory structure, and then
// packs it as a base64-encoded gzipped ISO9660 image.
func (configDrive ConfigDrive) ToConfigDrive() (string, error) {
directory, err := configDrive.ToDirectory()
if err != nil {
return "", err
}
defer os.RemoveAll(directory)

// Pack result as gzipped ISO9660 file
result, err := PackDirectoryAsISO(directory)
if err != nil {
return "", err
}

// Return as base64-encoded data
return base64.StdEncoding.EncodeToString(result), nil
}
35 changes: 35 additions & 0 deletions openstack/baremetal/v1/nodes/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nodes

/*
Package nodes provides utilities for working with Ironic's baremetal API.
* Building a config drive
As part of provisioning a node, you may need a config drive that contains user data, metadata, and network data
stored inside a base64-encoded gzipped ISO9660 file. These utilities can create that for you.
For example:
configDrive = nodes.ConfigDrive{
UserData: nodes.UserDataMap{
"ignition": map[string]string{
"version": "2.2.0",
},
"systemd": map[string]interface{}{
"units": []map[string]interface{}{{
"name": "example.service",
"enabled": true,
},
},
},
}
Then to upload this to Ironic as a using gophercloud:
err = nodes.ChangeProvisionState(client, uuid, nodes.ProvisionStateOpts{
Target: "active",
ConfigDrive: configDrive.ToConfigDrive(),
}).ExtractErr()
*/

64 changes: 64 additions & 0 deletions openstack/baremetal/v1/nodes/testing/configdrive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package testing

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

th "github.com/gophercloud/gophercloud/testhelper"
"github.com/gophercloud/utils/openstack/baremetal/v1/nodes"
)

func TestUserDataFromMap(t *testing.T) {
userData, err := IgnitionUserData.ToUserData()
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(userData), IgnitionUserData)
}

func TestUserDataFromString(t *testing.T) {
cloudInit := nodes.UserDataString(CloudInitString)
userData, err := cloudInit.ToUserData()
th.AssertNoErr(t, err)
th.AssertByteArrayEquals(t, userData, []byte(cloudInit))
}

func TestConfigDriveToDirectory(t *testing.T) {
path, err := ConfigDrive.ToDirectory()
th.AssertNoErr(t, err)
defer os.RemoveAll(path)

basePath := filepath.FromSlash(path + "/openstack/latest")

userData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/user_data"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(userData), IgnitionUserData)

metaData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/meta_data.json"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(metaData), OpenStackMetaData)

networkData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/network_data.json"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(networkData), NetworkData)
}

func TestConfigDriveVersionToDirectory(t *testing.T) {
path, err := ConfigDriveVersioned.ToDirectory()
th.AssertNoErr(t, err)
defer os.RemoveAll(path)

basePath := filepath.FromSlash(path + "/openstack/" + ConfigDriveVersioned.Version)

userData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/user_data"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(userData), IgnitionUserData)

metaData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/meta_data.json"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(metaData), OpenStackMetaData)

networkData, err := ioutil.ReadFile(filepath.FromSlash(basePath + "/network_data.json"))
th.AssertNoErr(t, err)
th.CheckJSONEquals(t, string(networkData), NetworkData)
}
101 changes: 101 additions & 0 deletions openstack/baremetal/v1/nodes/testing/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package testing

import "github.com/gophercloud/utils/openstack/baremetal/v1/nodes"

const IgnitionConfig = `
{
"ignition": {
"version": "2.2.0"
},
"systemd": {
"units": [
{
"enabled": true,
"name": "example.service"
}
]
}
}
`

const OpenstackMetaDataJSON = `
{
"availability_zone": "nova",
"hostname": "test.novalocal",
"public_keys": {
"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDBqUfVvCSez0/Wfpd8dLLgZXV9GtXQ7hnMN+Z0OWQUyebVEHey1CXuin0uY1cAJMhUq8j98SiW+cU0sU4J3x5l2+xi1bodDm1BtFWVeLIOQINpfV1n8fKjHB+ynPpe1F6tMDvrFGUlJs44t30BrujMXBe8Rq44cCk6wqyjATA3rQ== Generated by Nova\n"
}
}
`

const NetworkDataJSON = `
"services": [
{
"type": "dns",
"address": "8.8.8.8"
},
{
"type": "dns",
"address": "8.8.4.4"
}
]
`

const CloudInitString = `
#cloud-init
groups:
- ubuntu: [root,sys]
- cloud-users
`

var (
IgnitionUserData = nodes.UserDataMap{
"ignition": map[string]string{
"version": "2.2.0",
},
"systemd": map[string]interface{}{
"units": []map[string]interface{}{{
"name": "example.service",
"enabled": true,
},
},
},
}

OpenStackMetaData = map[string]interface{}{
"availability_zone": "nova",
"hostname": "test.novalocal",
"public_keys": map[string]string{
"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDBqUfVvCSez0/Wfpd8dLLgZXV9GtXQ7hnMN+Z0OWQUyebVEHey1CXuin0uY1cAJMhUq8j98SiW+cU0sU4J3x5l2+xi1bodDm1BtFWVeLIOQINpfV1n8fKjHB+ynPpe1F6tMDvrFGUlJs44t30BrujMXBe8Rq44cCk6wqyjATA3rQ== Generated by Nova\n",
},
}

NetworkData = map[string]interface{}{
"services": []map[string]string{
{
"type": "dns",
"address": "8.8.8.8",
},
{
"type": "dns",
"address": "8.8.4.4",
},
},
}

CloudInitUserData = nodes.UserDataString(CloudInitString)

ConfigDrive = nodes.ConfigDrive{
UserData: IgnitionUserData,
MetaData: OpenStackMetaData,
NetworkData: NetworkData,
}

ConfigDriveVersioned = nodes.ConfigDrive{
UserData: IgnitionUserData,
MetaData: OpenStackMetaData,
NetworkData: NetworkData,
Version: "2018-10-10",
}
)
59 changes: 59 additions & 0 deletions openstack/baremetal/v1/nodes/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package nodes

import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"os"
"os/exec"
)

// Gzips a file
func GzipFile(path string) ([]byte, error) {
var buf bytes.Buffer

w := gzip.NewWriter(&buf)
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
_, err = w.Write(contents)
if err != nil {
return nil, err
}

err = w.Close()
if err != nil {
return nil, err
}

result := buf.Bytes()
return result, nil
}

// Packs a directory into a gzipped ISO image
func PackDirectoryAsISO(path string) ([]byte, error) {
iso, err := ioutil.TempFile("", "gophercloud-iso")
if err != nil {
return nil, err
}
iso.Close()
defer os.Remove(iso.Name())
cmd := exec.Command(
"mkisofs",
"-o", iso.Name(),
"-ldots",
"-allow-lowercase",
"-allow-multidot", "-l",
"-publisher", "gophercloud",
"-quiet", "-J",
"-r", "-V", "config-2",
path,
)
if err = cmd.Run(); err != nil {
return nil, fmt.Errorf("error creating configdrive iso: %s", err.Error())
}

return GzipFile(iso.Name())
}

0 comments on commit 8594c32

Please sign in to comment.