Skip to content

Commit 03e6b48

Browse files
committed
provider/azure: enable CentOS support
Most of the changes here are refactoring the user-data rendering interfaces and implementations to support rendering to a script instead of to YAML. With the rendering changes in place, the azure provider changes are relatively trivial: start the OpenLogic CentOS 7.1 image with cloud-config rendered as a script in CustomData, and then execute it using the CustomScript extension as we do on Windows.
1 parent 2f43c1e commit 03e6b48

28 files changed

+373
-158
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2015 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package cloudinittest
5+
6+
import (
7+
"github.com/juju/testing"
8+
9+
"github.com/juju/juju/cloudconfig/cloudinit"
10+
)
11+
12+
type CloudConfig struct {
13+
cloudinit.CloudConfig
14+
testing.Stub
15+
16+
YAML []byte
17+
Script string
18+
}
19+
20+
func (c *CloudConfig) RenderYAML() ([]byte, error) {
21+
c.MethodCall(c, "RenderYAML")
22+
return c.YAML, c.NextErr()
23+
}
24+
25+
func (c *CloudConfig) RenderScript() (string, error) {
26+
c.MethodCall(c, "RenderScript")
27+
return c.Script, c.NextErr()
28+
}

cloudconfig/providerinit/providerinit.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudC
6565
}
6666
// This might get replaced by a renderer.RenderUserdata which will either
6767
// render it as YAML or Bash since some CentOS images might ship without cloudnit
68-
udata, err := cloudcfg.RenderYAML()
69-
if err != nil {
70-
return nil, errors.Trace(err)
71-
}
72-
udata, err = renderer.EncodeUserdata(udata, operatingSystem)
68+
udata, err := renderer.Render(cloudcfg, operatingSystem)
7369
if err != nil {
7470
return nil, errors.Trace(err)
7571
}

cloudconfig/providerinit/renderers/common.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"fmt"
1010

1111
"github.com/juju/juju/cloudconfig"
12+
"github.com/juju/juju/cloudconfig/cloudinit"
1213
"github.com/juju/utils"
14+
"github.com/juju/utils/os"
1315
)
1416

1517
// ToBase64 just transforms whatever userdata it gets to base64 format
@@ -32,3 +34,33 @@ func AddPowershellTags(udata []byte) []byte {
3234
string(udata) +
3335
`</powershell>`)
3436
}
37+
38+
// Decorator is a function that can be used as part of a rendering pipeline.
39+
type Decorator func([]byte) []byte
40+
41+
// RenderYAML renders the given cloud-config as YAML, and then passes the
42+
// YAML through the given decorators.
43+
func RenderYAML(cfg cloudinit.RenderConfig, os os.OSType, ds ...Decorator) ([]byte, error) {
44+
out, err := cfg.RenderYAML()
45+
if err != nil {
46+
return nil, err
47+
}
48+
return applyDecorators(out, ds), nil
49+
}
50+
51+
// RenderScript renders the given cloud-config as a script, and then passes the
52+
// script through the given decorators.
53+
func RenderScript(cfg cloudinit.RenderConfig, os os.OSType, ds ...Decorator) ([]byte, error) {
54+
out, err := cfg.RenderScript()
55+
if err != nil {
56+
return nil, err
57+
}
58+
return applyDecorators([]byte(out), ds), nil
59+
}
60+
61+
func applyDecorators(out []byte, ds []Decorator) []byte {
62+
for _, d := range ds {
63+
out = d(out)
64+
}
65+
return out
66+
}

cloudconfig/providerinit/renderers/common_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010

1111
jc "github.com/juju/testing/checkers"
1212
"github.com/juju/utils"
13+
"github.com/juju/utils/os"
1314
gc "gopkg.in/check.v1"
1415

1516
"github.com/juju/juju/cloudconfig"
17+
"github.com/juju/juju/cloudconfig/cloudinit/cloudinittest"
1618
"github.com/juju/juju/cloudconfig/providerinit/renderers"
1719
"github.com/juju/juju/testing"
1820
)
@@ -43,3 +45,31 @@ func (s *RenderersSuite) TestAddPowershellTags(c *gc.C) {
4345
out := renderers.AddPowershellTags(in)
4446
c.Assert(out, jc.DeepEquals, expected)
4547
}
48+
49+
func (s *RenderersSuite) TestRenderYAML(c *gc.C) {
50+
cloudcfg := &cloudinittest.CloudConfig{YAML: []byte("yaml")}
51+
d1 := func(in []byte) []byte {
52+
return []byte("1." + string(in))
53+
}
54+
d2 := func(in []byte) []byte {
55+
return []byte("2." + string(in))
56+
}
57+
out, err := renderers.RenderYAML(cloudcfg, os.Windows, d2, d1)
58+
c.Assert(err, jc.ErrorIsNil)
59+
c.Assert(string(out), jc.DeepEquals, "1.2.yaml")
60+
cloudcfg.CheckCallNames(c, "RenderYAML")
61+
}
62+
63+
func (s *RenderersSuite) TestRenderScript(c *gc.C) {
64+
cloudcfg := &cloudinittest.CloudConfig{Script: "script"}
65+
d1 := func(in []byte) []byte {
66+
return []byte("1." + string(in))
67+
}
68+
d2 := func(in []byte) []byte {
69+
return []byte("2." + string(in))
70+
}
71+
out, err := renderers.RenderScript(cloudcfg, os.Windows, d2, d1)
72+
c.Assert(err, jc.ErrorIsNil)
73+
c.Assert(string(out), jc.DeepEquals, "1.2.script")
74+
cloudcfg.CheckCallNames(c, "RenderScript")
75+
}

cloudconfig/providerinit/renderers/interface.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ package renderers
1010

1111
import (
1212
"github.com/juju/utils/os"
13+
14+
"github.com/juju/juju/cloudconfig/cloudinit"
1315
)
1416

1517
// ProviderRenderer defines a method to encode userdata depending on
@@ -18,8 +20,5 @@ import (
1820
// the userdata differently(bash vs yaml) since some providers might
1921
// not ship cloudinit on every OS
2022
type ProviderRenderer interface {
21-
22-
// EncodeUserdata takes a []byte and encodes it in the right format.
23-
// The implementations are based on the different providers and OSTypes.
24-
EncodeUserdata([]byte, os.OSType) ([]byte, error)
23+
Render(cloudinit.CloudConfig, os.OSType) ([]byte, error)
2524
}

provider/azure/environ.go

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ func createVirtualMachine(
542542
return compute.VirtualMachine{}, errors.Annotate(err, "creating storage profile")
543543
}
544544

545-
osProfile, err := newOSProfile(vmName, instanceConfig)
545+
osProfile, seriesOS, err := newOSProfile(vmName, instanceConfig)
546546
if err != nil {
547547
return compute.VirtualMachine{}, errors.Annotate(err, "creating OS profile")
548548
}
@@ -588,37 +588,19 @@ func createVirtualMachine(
588588
return compute.VirtualMachine{}, errors.Annotate(err, "creating virtual machine")
589589
}
590590

591-
// On Windows, we must add the CustomScriptExtension VM extension
592-
// to run the CustomData script.
593-
if osProfile.WindowsConfiguration != nil {
594-
const extensionName = "JujuCustomScriptExtension"
595-
extensionSettings := map[string]*string{
596-
"commandToExecute": to.StringPtr(
597-
`move C:\AzureData\CustomData.bin C:\AzureData\CustomData.ps1 && ` +
598-
`powershell.exe -ExecutionPolicy Unrestricted -File C:\AzureData\CustomData.ps1 && ` +
599-
`del /q C:\AzureData\CustomData.ps1`,
600-
),
601-
}
602-
extension := compute.VirtualMachineExtension{
603-
Location: to.StringPtr(location),
604-
Tags: toTagsPtr(vmTags),
605-
Properties: &compute.VirtualMachineExtensionProperties{
606-
Publisher: to.StringPtr("Microsoft.Compute"),
607-
Type: to.StringPtr("CustomScriptExtension"),
608-
TypeHandlerVersion: to.StringPtr("1.4"),
609-
AutoUpgradeMinorVersion: to.BoolPtr(true),
610-
Settings: &extensionSettings,
611-
},
612-
}
613-
if _, err := vmExtensionClient.CreateOrUpdate(
614-
resourceGroup, vmName, extensionName, extension,
591+
// On Windows and CentOS, we must add the CustomScript VM
592+
// extension to run the CustomData script.
593+
switch seriesOS {
594+
case os.Windows, os.CentOS:
595+
if err := createVMExtension(
596+
vmExtensionClient, seriesOS,
597+
resourceGroup, vmName, location, vmTags,
615598
); err != nil {
616599
return compute.VirtualMachine{}, errors.Annotate(
617-
err, "creating CustomScript extension",
600+
err, "creating virtual machine extension",
618601
)
619602
}
620603
}
621-
622604
return vm, nil
623605
}
624606

@@ -754,12 +736,12 @@ func newStorageProfile(
754736
}, nil
755737
}
756738

757-
func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*compute.OSProfile, error) {
739+
func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*compute.OSProfile, os.OSType, error) {
758740
logger.Debugf("creating OS profile for %q", vmName)
759741

760742
customData, err := providerinit.ComposeUserData(instanceConfig, nil, AzureRenderer{})
761743
if err != nil {
762-
return nil, errors.Annotate(err, "composing user data")
744+
return nil, os.Unknown, errors.Annotate(err, "composing user data")
763745
}
764746

765747
osProfile := &compute.OSProfile{
@@ -769,7 +751,7 @@ func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*c
769751

770752
seriesOS, err := jujuseries.GetOSFromSeries(instanceConfig.Series)
771753
if err != nil {
772-
return nil, errors.Trace(err)
754+
return nil, os.Unknown, errors.Trace(err)
773755
}
774756
switch seriesOS {
775757
case os.Ubuntu, os.CentOS, os.Arch:
@@ -797,9 +779,9 @@ func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*c
797779
// TODO(?) add WinRM configuration here.
798780
}
799781
default:
800-
return nil, errors.NotSupportedf("%s", seriesOS)
782+
return nil, os.Unknown, errors.NotSupportedf("%s", seriesOS)
801783
}
802-
return osProfile, nil
784+
return osProfile, seriesOS, nil
803785
}
804786

805787
// StopInstances is specified in the InstanceBroker interface.

provider/azure/internal/imageutils/images.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import (
2424
var logger = loggo.GetLogger("juju.provider.azure")
2525

2626
const (
27+
centOSPublisher = "OpenLogic"
28+
centOSOffering = "CentOS"
29+
2730
ubuntuPublisher = "Canonical"
2831
ubuntuOffering = "UbuntuServer"
2932

@@ -57,6 +60,7 @@ func SeriesImage(
5760
if err != nil {
5861
return nil, errors.Annotatef(err, "selecting SKU for %s", series)
5962
}
63+
6064
case os.Windows:
6165
publisher = windowsPublisher
6266
offering = windowsOffering
@@ -65,7 +69,20 @@ func SeriesImage(
6569
sku = "2012-Datacenter"
6670
case "win2012r2":
6771
sku = "2012-R2-Datacenter"
72+
default:
73+
return nil, errors.NotSupportedf("deploying %s", series)
74+
}
75+
76+
case os.CentOS:
77+
publisher = centOSPublisher
78+
offering = centOSOffering
79+
switch series {
80+
case "centos7":
81+
sku = "7.1"
82+
default:
83+
return nil, errors.NotSupportedf("deploying %s", series)
6884
}
85+
6986
default:
7087
// TODO(axw) CentOS
7188
return nil, errors.NotSupportedf("deploying %s", seriesOS)

provider/azure/internal/imageutils/images_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ func (s *imageutilsSuite) TestSeriesImageWindows(c *gc.C) {
6464
}
6565

6666
func (s *imageutilsSuite) TestSeriesImageCentOS(c *gc.C) {
67-
_, err := imageutils.SeriesImage("centos7", "released", "westus", s.client)
68-
c.Assert(err, gc.ErrorMatches, "deploying CentOS not supported")
67+
s.assertImageId(c, "centos7", "released", "OpenLogic:CentOS:7.1:latest")
68+
}
69+
70+
func (s *imageutilsSuite) TestSeriesImageArch(c *gc.C) {
71+
_, err := imageutils.SeriesImage("arch", "released", "westus", s.client)
72+
c.Assert(err, gc.ErrorMatches, "deploying Arch not supported")
6973
}
7074

7175
func (s *imageutilsSuite) TestSeriesImageStream(c *gc.C) {

provider/azure/internal/legacy/azure/userdata.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/juju/utils"
1010
"github.com/juju/utils/os"
1111

12+
"github.com/juju/juju/cloudconfig/cloudinit"
1213
"github.com/juju/juju/cloudconfig/providerinit/renderers"
1314
)
1415

@@ -25,7 +26,11 @@ rm C:\AzureData\CustomData.ps1
2526

2627
type AzureRenderer struct{}
2728

28-
func (AzureRenderer) EncodeUserdata(udata []byte, vers os.OSType) ([]byte, error) {
29+
func (AzureRenderer) Render(cfg cloudinit.CloudConfig, vers os.OSType) ([]byte, error) {
30+
udata, err := cfg.RenderYAML()
31+
if err != nil {
32+
return nil, err
33+
}
2934
switch vers {
3035
case os.Ubuntu, os.CentOS:
3136
return renderers.ToBase64(utils.Gzip(udata)), nil

provider/azure/internal/legacy/azure/userdata_test.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/juju/utils/os"
1313
gc "gopkg.in/check.v1"
1414

15+
"github.com/juju/juju/cloudconfig/cloudinit/cloudinittest"
1516
"github.com/juju/juju/cloudconfig/providerinit/renderers"
1617
"github.com/juju/juju/provider/azure/internal/legacy/azure"
1718
"github.com/juju/juju/testing"
@@ -25,30 +26,32 @@ var _ = gc.Suite(&UserdataSuite{})
2526

2627
func (s *UserdataSuite) TestAzureUnix(c *gc.C) {
2728
renderer := azure.AzureRenderer{}
28-
data := []byte("test")
29-
result, err := renderer.EncodeUserdata(data, os.Ubuntu)
29+
cloudcfg := &cloudinittest.CloudConfig{YAML: []byte("yaml")}
30+
31+
result, err := renderer.Render(cloudcfg, os.Ubuntu)
3032
c.Assert(err, jc.ErrorIsNil)
31-
expected := base64.StdEncoding.EncodeToString(utils.Gzip(data))
33+
expected := base64.StdEncoding.EncodeToString(utils.Gzip(cloudcfg.YAML))
3234
c.Assert(string(result), jc.DeepEquals, expected)
3335

34-
data = []byte("test")
35-
result, err = renderer.EncodeUserdata(data, os.CentOS)
36+
result, err = renderer.Render(cloudcfg, os.CentOS)
3637
c.Assert(err, jc.ErrorIsNil)
37-
expected = base64.StdEncoding.EncodeToString(utils.Gzip(data))
3838
c.Assert(string(result), jc.DeepEquals, expected)
3939
}
4040

4141
func (s *UserdataSuite) TestAzureWindows(c *gc.C) {
4242
renderer := azure.AzureRenderer{}
43-
data := []byte("test")
44-
result, err := renderer.EncodeUserdata(data, os.Windows)
43+
cloudcfg := &cloudinittest.CloudConfig{YAML: []byte("yaml")}
44+
45+
result, err := renderer.Render(cloudcfg, os.Windows)
4546
c.Assert(err, jc.ErrorIsNil)
46-
c.Assert(result, jc.DeepEquals, renderers.ToBase64(renderers.WinEmbedInScript(data)))
47+
expected := base64.StdEncoding.EncodeToString(renderers.WinEmbedInScript(cloudcfg.YAML))
48+
c.Assert(string(result), jc.DeepEquals, expected)
4749
}
4850

4951
func (s *UserdataSuite) TestAzureUnknownOS(c *gc.C) {
5052
renderer := azure.AzureRenderer{}
51-
result, err := renderer.EncodeUserdata(nil, os.Arch)
52-
c.Assert(result, gc.IsNil)
53+
cloudcfg := &cloudinittest.CloudConfig{YAML: []byte("yaml")}
54+
55+
_, err := renderer.Render(cloudcfg, os.Arch)
5356
c.Assert(err, gc.ErrorMatches, "Cannot encode userdata for OS: Arch")
5457
}

0 commit comments

Comments
 (0)