Skip to content

Commit

Permalink
Support to add a remote (#35)
Browse files Browse the repository at this point in the history
This commit enables the LXD provider to add a remote if it has not
yet been added to the client configuration. This requires the user
to explicitly set accept_remote_certificate in the provider
configuration.

An option to generate the client certificates has also been added.
  • Loading branch information
jtopjian authored and sl1pm4t committed Apr 15, 2017
1 parent f712a12 commit 40fd7e3
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 25 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ To generate these files and store them in the LXD daemon, follow these [steps](h

```hcl
provider "lxd" {
scheme = "https"
address = "10.1.1.8"
remote = "lxd-server"
scheme = "https"
address = "10.1.1.8"
remote = "lxd-server"
remote_password = "password"
generate_client_certificates = true
accept_server_certificate = true
}
```

Expand Down Expand Up @@ -257,6 +260,8 @@ _note_: `local` and `remote` accept both IPv4 and IPv6 addresses.
* `remote` - *Optional* - Name of the remote LXD as it exists in the local lxc config. Defaults to `local`.
* `remote_password` - *Optional* - Password of the remote LXD server.
* `config_dir` - *Optional* - Directory path to client LXD configuration and certs. Defaults to `$HOME/.config/lxc`.
* `generate_client_certificates` - *Optional* - Generate the LXC client's certificates if they don't exist. This can also be done out-of-band of Terraform with the lxc command-line client.
* `accept_remote_certificate` - *Optional* - Accept the remote LXD server certificate. This can also be done out-of-band of Terraform with the lxc command-line client.

### Resources

Expand Down
75 changes: 55 additions & 20 deletions lxd/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func Provider() terraform.ResourceProvider {
Type: schema.TypeString,
Sensitive: true,
Optional: true,
Description: descriptions["lxc_remote_password"],
Description: descriptions["lxd_remote_password"],
DefaultFunc: schema.EnvDefaultFunc("LXD_REMOTE_PASSWORD", ""),
},

Expand All @@ -69,6 +69,20 @@ func Provider() terraform.ResourceProvider {
return os.ExpandEnv("$HOME/.config/lxc"), nil
},
},

"generate_client_certificates": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: descriptions["lxd_generate_client_certs"],
DefaultFunc: schema.EnvDefaultFunc("LXD_GENERATE_CLIENT_CERTS", ""),
},

"accept_remote_certificate": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: descriptions["lxd_accept_remote_certificate"],
DefaultFunc: schema.EnvDefaultFunc("LXD_ACCEPT_SERVER_CERTIFICATE", ""),
},
},

ResourcesMap: map[string]*schema.Resource{
Expand All @@ -85,12 +99,14 @@ var descriptions map[string]string

func init() {
descriptions = map[string]string{
"lxd_address": "The FQDN or IP where the LXD daemon can be contacted. (default = /var/lib/lxd/unix.socket)",
"lxd_scheme": "unix or https (default = unix)",
"lxd_port": "Port LXD Daemon is listening on (default 8443).",
"lxd_remote": "Name of the LXD remote. Required when lxd_scheme set to https, to enable locating server certificate.",
"lxd_remote_password": "The password for the remote.",
"lxd_config_dir": "The directory to look for existing LXD configuration (default = $HOME/.config/lxc).",
"lxd_address": "The FQDN or IP where the LXD daemon can be contacted. (default = /var/lib/lxd/unix.socket)",
"lxd_scheme": "unix or https (default = unix)",
"lxd_port": "Port LXD Daemon is listening on (default 8443).",
"lxd_remote": "Name of the LXD remote. Required when lxd_scheme set to https, to enable locating server certificate.",
"lxd_remote_password": "The password for the remote.",
"lxd_config_dir": "The directory to look for existing LXD configuration (default = $HOME/.config/lxc).",
"lxd_generate_client_certificates": "Automatically generate the LXD client certificates if they don't exist.",
"lxd_accept_remote_certificate": "Accept the server certificate",
}
}

Expand Down Expand Up @@ -118,13 +134,24 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
log.Printf("[DEBUG] LXD Config: %#v", config)

if scheme == "https" {
// validate certifictes exist
// Validate the client certificates or try to generate them.
certf := config.ConfigPath("client.crt")
keyf := config.ConfigPath("client.key")
if !shared.PathExists(certf) || !shared.PathExists(keyf) {
err := fmt.Errorf("Certificate or key not found:\n\t%s\n\t%s", certf, keyf)
return nil, err
if v, ok := d.Get("generate_client_certificates").(bool); ok && v {
log.Printf("[DEBUG] Attempting to generate client certificates")
if err := shared.FindOrGenCert(certf, keyf, true); err != nil {
return nil, err
}
} else {
err := fmt.Errorf("Certificate or key not found:\n\t%s\n\t%s\n"+
"Either set generate_client_certs to true or generate the "+
"certificates out of band of Terraform and try again", certf, keyf)
return nil, err
}
}

// Validate the server certificate or try to add the remote server.
serverCertf := config.ServerCertPath(remote)
if !shared.PathExists(serverCertf) {
// If the server certificate was not found, try to add the remote.
Expand Down Expand Up @@ -170,23 +197,31 @@ func validateClient(client *lxd.Client) error {
func addRemote(d *schema.ResourceData, config *lxd.Config) error {
// First, validate the client.
remote := d.Get("remote").(string)
c, err := lxd.NewClient(config, remote)
client, err := lxd.NewClient(config, remote)
if err != nil {
return err
}

// Check if the client is valid.
// Right now, this assumes PKI is in place by previous provisioning
// of client, server, and CA certificates.
// The LXC command-line client would prompt to accept a certificate
// if PKI was not in place.
_, err = c.GetServerConfig()
if err != nil {
return err
// If this passes, either the certificate was already accepted
// or the client is using PKI.
// If there is an error, attempt to accept the certificate.
if _, err = client.GetServerConfig(); err != nil {
if v, ok := d.Get("accept_remote_certificate").(bool); ok && v {
var err error
client, err = addServer(client, remote)
if err != nil {
return fmt.Errorf("Could not add the LXD server: %s", err)
}
} else {
return fmt.Errorf("Unable to communicate with remote. Either set " +
"accept_remote_certificate to true or add the remote out of band " +
"of Terraform and try again.")
}
}

// If the config is valid, check and see if the client is already trusted
if c.AmTrusted() {
if client.AmTrusted() {
log.Printf("[DEBUG] LXC client is trusted with %s", remote)
return nil
}
Expand All @@ -198,7 +233,7 @@ func addRemote(d *schema.ResourceData, config *lxd.Config) error {
}

log.Printf("[DEBUG] Attempting to authenticate with remote %s", remote)
_, err = clientDoUpdateMethod(c, "POST", "certificates", body, api.SyncResponse)
_, err = clientDoUpdateMethod(client, "POST", "certificates", body, api.SyncResponse)

if err != nil {
log.Printf("[DEBUG] Failed to authenticate with remote %s", remote)
Expand Down
75 changes: 73 additions & 2 deletions lxd/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package lxd

import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"net/http"
"os"
"strings"

"github.com/lxc/lxd"
"github.com/lxc/lxd/shared"
"github.com/lxc/lxd/shared/api"
"github.com/lxc/lxd/shared/version"
)
Expand Down Expand Up @@ -65,6 +69,46 @@ func resourceLxdValidateDeviceType(v interface{}, k string) (ws []string, errors
return
}

// addServer adds a remote server to the local LXD configuration.
func addServer(client *lxd.Client, remote string) (*lxd.Client, error) {
addr := client.Config.Remotes[remote]

log.Printf("[DEBUG] Attempting to retrieve remote server certificate")
var certificate *x509.Certificate
var err error
certificate, err = getRemoteCertificate(addr.Addr)
if err != nil {
return nil, err
}

dnam := client.Config.ConfigPath("servercerts")
if err := os.MkdirAll(dnam, 0750); err != nil {
return nil, fmt.Errorf("Could not create server cert dir")
}

certf := fmt.Sprintf("%s/%s.crt", dnam, client.Name)
certOut, err := os.Create(certf)
if err != nil {
return nil, err
}

pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
certOut.Close()

// Setup a new connection, this time with the remote certificate
client, err = lxd.NewClient(&client.Config, remote)
if err != nil {
return nil, err
}

// Validate the client before returning
if _, err := client.GetServerConfig(); err != nil {
return nil, err
}

return client, nil
}

// The following re-implements private LXC client functions
func clientURL(baseURL string, elem ...string) string {
// Normalize the URL
Expand Down Expand Up @@ -106,8 +150,6 @@ func clientDoUpdateMethod(client *lxd.Client, method string, base string, args i
return nil, err
}

log.Printf("[DEBUG] %s %s to %s", method, buf.String(), uri)

req, err := http.NewRequest(method, uri, &buf)
if err != nil {
return nil, err
Expand All @@ -122,3 +164,32 @@ func clientDoUpdateMethod(client *lxd.Client, method string, base string, args i

return lxd.HoistResponse(resp, rtype)
}

func getRemoteCertificate(address string) (*x509.Certificate, error) {
// Setup a permissive TLS config
tlsConfig, err := shared.GetTLSConfig("", "", "", nil)
if err != nil {
return nil, err
}

tlsConfig.InsecureSkipVerify = true
tr := &http.Transport{
TLSClientConfig: tlsConfig,
Dial: shared.RFC3493Dialer,
Proxy: shared.ProxyFromEnvironment,
}

// Connect
client := &http.Client{Transport: tr}
resp, err := client.Get(address)
if err != nil {
return nil, err
}

// Retrieve the certificate
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
return nil, fmt.Errorf("Unable to read remote TLS certificate")
}

return resp.TLS.PeerCertificates[0], nil
}

0 comments on commit 40fd7e3

Please sign in to comment.