Skip to content

Commit 3f106d3

Browse files
committed
Add documentation and fix minor bugs in SSH bootstrap process.
1 parent edc1750 commit 3f106d3

File tree

7 files changed

+370
-99
lines changed

7 files changed

+370
-99
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ fmt:
77
dev: version fmt
88
go build -o _bin/docker-machine-driver-ddcloud
99

10+
install: dev
11+
go install
12+
1013
# Perform a full (all-platforms) build.
1114
build: version build-windows64 build-linux64 build-mac64
1215

@@ -29,4 +32,4 @@ test: fmt
2932
go test -v github.com/DimensionDataResearch/docker-machine-driver-ddcloud/...
3033

3134
version:
32-
echo "package main\n\n// ProviderVersion is the current version of the CloudControl driver for Docker Machine.\nconst ProviderVersion = \"v0.1 (`git rev-parse HEAD`)\"" > ./version-info.go
35+
echo "package main\n\n// DriverVersion is the current version of the CloudControl driver for Docker Machine.\nconst DriverVersion = \"v0.1 (`git rev-parse HEAD`)\"" > ./version-info.go

README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,59 @@
1-
# docker-machine-driver-ddcloud
2-
Docker Machine driver for Dimension Data CloudControl.
1+
# Docker Machine driver for Dimension Data CloudControl
2+
3+
## Usage
4+
5+
You will need:
6+
7+
* A network domain
8+
* A VLAN in that network domain (servers will be attached to this VLAN)
9+
* A firewall rule that permits SSH traffic from your (local) public IPv4 address to the VLAN's IPv4 network
10+
11+
The driver will allocate a public IP address and NAT rule for each machine that it creates.
12+
13+
### Example
14+
15+
```bash
16+
docker-machine create --driver ddcloud \
17+
--ddcloud-region AU \
18+
--ddcloud-datacenter AU9 \
19+
--ddcloud-networkdomain 'my-docker-domain' \
20+
--ddcloud-vlan 'my-docker-vlan' \
21+
--ddcloud-ssh-key ~/.ssh/id_rsa \
22+
--ddcloud-ssh-bootstrap-password 'throw-away-password' \
23+
mydockermachine
24+
```
25+
26+
If you're running on Windows, just remove the backslashes so the whole command is on a single line.
27+
28+
### Options
29+
30+
The driver supports all Docker Machine commands, and can be configured using the following command-line arguments (or environment variables):
31+
32+
* `ddcloud-user` - The user name used to authenticate to the CloudControl API.
33+
Environment: `DD_COMPUTE_USER`
34+
* `ddcloud-password` - The password used to authenticate to the CloudControl API.
35+
Environment: `DD_COMPUTE_PASSWORD`.
36+
* `ddcloud-region` - The CloudControl region name (e.g. AU, NA, EU, etc).
37+
Environment: `DD_COMPUTE_REGION`.
38+
* `ddcloud-networkdomain` - The name of the target CloudControl network domain.
39+
* `ddcloud-datacenter` - The name of the CloudControl datacenter (e.g. NA1, AU9) in which the network domain is located.
40+
* `ddcloud-vlan` - The name of the target CloudControl VLAN.
41+
* `ddcloud-ssh-user` - The SSH username to use.
42+
Default: "root".
43+
Environment: `DD_COMPUTE_SSH_USER`
44+
* `ddcloud-ssh-key` - The SSH key file to use.
45+
Environment: `DD_COMPUTE_SSH_KEY`
46+
* `ddcloud-ssh-port` - The SSH port to use.
47+
Default: 22.
48+
Environment: `DD_COMPUTE_SSH_PORT`
49+
* `ddcloud-ssh-bootstrap-password` - The initial SSH password used to bootstrap SSH key authentication.
50+
This password is removed once the SSH key has been installed
51+
Environment: `DD_COMPUTE_SSH_BOOTSTRAP_PASSWORD`
52+
53+
## Installing the provider
54+
55+
Download the [latest release](https://github.com/DimensionDataResearch/docker-machine-driver-ddcloud/releases) and place the provider executable in the same directory as `docker-machine` executable (or somewhere on your `PATH`).
56+
57+
## Building the provider
58+
59+
If you'd rather run from source, simply run `make install` and you're good to go.

client.go

Lines changed: 153 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ func (driver *Driver) getCloudControlClient() (client *compute.Client, err error
3333
return
3434
}
3535

36-
driver.client = compute.NewClient(driver.CloudControlRegion, driver.CloudControlUser, driver.CloudControlPassword)
36+
client = compute.NewClient(driver.CloudControlRegion, driver.CloudControlUser, driver.CloudControlPassword)
37+
client.ConfigureRetry(10, 5*time.Second)
38+
39+
driver.client = client
3740

3841
return
3942
}
@@ -115,34 +118,36 @@ func (driver *Driver) getVLAN() (*compute.VLAN, error) {
115118
return client.GetVLAN(driver.VLANID)
116119
}
117120

118-
// Resolve (find) the target OS image.
119-
func (driver *Driver) resolveOSImage() error {
120-
driver.ImageID = ""
121+
// Resolve (find) the target network domain by name and data centre Id.
122+
func (driver *Driver) resolveVLAN() error {
123+
driver.VLANID = ""
121124

122-
networkDomain, err := driver.getNetworkDomain()
123-
if err != nil {
124-
return err
125+
if driver.VLANName == "" {
126+
return errors.New("VLAN name has not been configured")
127+
}
128+
129+
var err error
130+
if driver.NetworkDomainID == "" {
131+
err = driver.resolveNetworkDomain()
132+
if err != nil {
133+
return err
134+
}
125135
}
126136

127137
client, err := driver.getCloudControlClient()
128138
if err != nil {
129139
return err
130140
}
131141

132-
// Find target OS image.
133-
log.Info("Searching for image '%s' in data centre '%s'...", driver.ImageName, networkDomain.DatacenterID)
134-
135-
image, err := client.FindOSImage(driver.ImageName, networkDomain.DatacenterID)
136-
if err == nil {
142+
vlan, err := client.GetVLANByName(driver.VLANName, driver.NetworkDomainID)
143+
if err != nil {
137144
return err
138145
}
139-
if image == nil {
140-
log.Errorf("OS image '%s' was not found in data centre '%s'.", driver.ImageName, networkDomain.DatacenterID)
141-
142-
return fmt.Errorf("OS image '%s' was not found in data centre '%s'", driver.ImageName, networkDomain.DatacenterID)
146+
if vlan == nil {
147+
return fmt.Errorf("No VLAN named '%s' was found in network domain '%s' ('%s')", driver.VLANName, driver.NetworkDomainName, driver.NetworkDomainID)
143148
}
144149

145-
driver.ImageID = image.ID
150+
driver.VLANID = vlan.ID
146151

147152
return nil
148153
}
@@ -161,6 +166,35 @@ func (driver *Driver) getOSImage() (*compute.OSImage, error) {
161166
return client.GetOSImage(driver.ImageID)
162167
}
163168

169+
// Resolve (find) the target OS image.
170+
func (driver *Driver) resolveOSImage() error {
171+
driver.ImageID = ""
172+
173+
networkDomain, err := driver.getNetworkDomain()
174+
if err != nil {
175+
return err
176+
}
177+
178+
client, err := driver.getCloudControlClient()
179+
if err != nil {
180+
return err
181+
}
182+
183+
image, err := client.FindOSImage(driver.ImageName, driver.DataCenterID)
184+
if err != nil {
185+
return err
186+
}
187+
if image == nil {
188+
log.Errorf("OS image '%s' was not found in data centre '%s'.", driver.ImageName, networkDomain.DatacenterID)
189+
190+
return fmt.Errorf("OS image '%s' was not found in data centre '%s'", driver.ImageName, networkDomain.DatacenterID)
191+
}
192+
193+
driver.ImageID = image.ID
194+
195+
return nil
196+
}
197+
164198
func (driver *Driver) deployServer() (*compute.Server, error) {
165199
if driver.isServerCreated() {
166200
return nil, fmt.Errorf("Server '%s' already exists (Id = '%s')", driver.MachineName, driver.ServerID)
@@ -181,17 +215,50 @@ func (driver *Driver) deployServer() (*compute.Server, error) {
181215
return nil, err
182216
}
183217

184-
log.Debug("Deploying server '%s' ('%s')...", driver.ServerID, driver.MachineName)
218+
log.Debugf("Deploying server '%s' ('%s')...", driver.ServerID, driver.MachineName)
185219

186220
resource, err := client.WaitForDeploy(compute.ResourceTypeServer, driver.ServerID, 15*time.Minute)
187221
if err != nil {
188222
return nil, err
189223
}
190224
server := resource.(*compute.Server)
191225

192-
log.Debug("Server '%s' ('%s') has been successfully provisioned...", driver.ServerID, server.Name)
226+
log.Debugf("Server '%s' ('%s') has been successfully deployed...", driver.ServerID, server.Name)
227+
228+
driver.PrivateIPAddress = *server.Network.PrimaryAdapter.PrivateIPv4Address
229+
230+
log.Debugf("Creating NAT rule for server '%s' ('%s')...", driver.MachineName, driver.PrivateIPAddress)
231+
232+
natRule, err := driver.getExistingNATRuleByInternalIP(driver.PrivateIPAddress)
233+
if natRule == nil {
234+
err = driver.ensurePublicIPAvailable()
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
natRuleID, err := client.AddNATRule(driver.NetworkDomainID, driver.PrivateIPAddress, nil)
240+
if err != nil {
241+
return nil, err
242+
}
243+
natRule, err = client.GetNATRule(natRuleID)
244+
if err != nil {
245+
return nil, err
246+
}
247+
if natRule == nil {
248+
return nil, fmt.Errorf("Failed to retrieve newly-created NAT rule '%s' for server '%s'", natRuleID, driver.MachineName)
249+
}
250+
} else {
251+
log.Debugf("NAT rule already exists (Id = '%s').", natRule.ID)
252+
}
253+
254+
driver.IPAddress = natRule.ExternalIPAddress
193255

194-
driver.IPAddress = *server.Network.PrimaryAdapter.PrivateIPv4Address
256+
log.Debugf("Created NAT rule '%s' for server '%s' (Ext:'%s' -> Int:'%s').",
257+
driver.NATRuleID,
258+
driver.MachineName,
259+
driver.IPAddress,
260+
driver.PrivateIPAddress,
261+
)
195262

196263
return server, nil
197264
}
@@ -229,3 +296,69 @@ func (driver *Driver) buildDeploymentConfiguration() (deploymentConfiguration co
229296

230297
return
231298
}
299+
300+
// Ensure that at least one public IP address is available in the target network domain.
301+
func (driver *Driver) ensurePublicIPAvailable() error {
302+
if driver.NetworkDomainID == "" {
303+
return errors.New("Network domain has not been resolved.")
304+
}
305+
306+
log.Debugf("Verifying that network domain '%s' has a public IP available for server '%s'...", driver.NetworkDomainName, driver.MachineName)
307+
308+
client, err := driver.getCloudControlClient()
309+
if err != nil {
310+
return err
311+
}
312+
313+
availableIPs, err := client.GetAvailablePublicIPAddresses(driver.NetworkDomainID)
314+
if err != nil {
315+
return err
316+
}
317+
318+
if len(availableIPs) == 0 {
319+
log.Debugf("There are no available public IPs in network domain '%s'; a new block of public IPs will be allocated.", driver.NetworkDomainID)
320+
321+
blockID, err := client.AddPublicIPBlock(driver.NetworkDomainID)
322+
if err != nil {
323+
return err
324+
}
325+
326+
log.Debugf("Allocated new public IP block '%s'.", blockID)
327+
}
328+
329+
return nil
330+
}
331+
332+
// Find the existing NAT rule (if any) for the specified internal IPv4 address.
333+
func (driver *Driver) getExistingNATRuleByInternalIP(internalIPAddress string) (*compute.NATRule, error) {
334+
if driver.NetworkDomainID == "" {
335+
return nil, errors.New("Network domain has not been resolved.")
336+
}
337+
338+
client, err := driver.getCloudControlClient()
339+
if err != nil {
340+
return nil, err
341+
}
342+
343+
page := compute.DefaultPaging()
344+
for {
345+
var rules *compute.NATRules
346+
rules, err = client.ListNATRules(driver.NetworkDomainID, page)
347+
if err != nil {
348+
return nil, err
349+
}
350+
if rules.IsEmpty() {
351+
break // We're done
352+
}
353+
354+
for _, rule := range rules.Rules {
355+
if rule.InternalIPAddress == internalIPAddress {
356+
return &rule, nil
357+
}
358+
}
359+
360+
page.Next()
361+
}
362+
363+
return nil, nil
364+
}

0 commit comments

Comments
 (0)