├── .gitignore ├── parts ├── outputs.t ├── base.t ├── vars.t ├── params.t └── resources.t ├── pkg ├── engine │ ├── types.go │ ├── fileloader.go │ ├── ssh.go │ ├── transform │ │ ├── transform_test.go │ │ ├── json.go │ │ └── apimodel_merger.go │ ├── filesaver.go │ ├── output.go │ ├── defaults.go │ ├── pki_test.go │ ├── params.go │ ├── pki.go │ ├── ssh_test.go │ ├── template_generator.go │ └── engine.go ├── helpers │ ├── helpers_test.go │ └── helpers.go └── api │ ├── common │ ├── net.go │ ├── helper.go │ ├── net_test.go │ ├── versions.go │ └── versions_test.go │ ├── const.go │ ├── strictjson.go │ ├── apiloader.go │ ├── cvm.go │ ├── tvm.go │ ├── strictjson_test.go │ ├── types.go │ └── validate.go ├── test ├── tvm-ub1804.json ├── cvm-win2022.json ├── cvm-win-ec-encrypted.json ├── cvm-win2022-encrypted.json ├── cvm-ubuntu.json ├── cvm-win.json ├── cvm-win2019.json ├── cvm-win2019-encrypted.json ├── cvm-win-ubuntu.json ├── cvm-linux2004.json ├── cvm-linux1804.json ├── cvm-linux1804-east-us.json ├── cvm-linux1804-encrypted.json └── cvm-linux2004-encrypted.json ├── Makefile ├── cmd └── acc-vm-engine │ ├── main.go │ └── generate.go ├── go.mod ├── LICENSE.txt ├── SECURITY.md ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/engine/templates.go 2 | /acc-vm-engine 3 | /_output 4 | /vendor 5 | /.DS_Store 6 | -------------------------------------------------------------------------------- /parts/outputs.t: -------------------------------------------------------------------------------- 1 | "adminUsername": { 2 | "type": "string", 3 | "value": "[parameters('adminUsername')]" 4 | } 5 | -------------------------------------------------------------------------------- /pkg/engine/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package engine 5 | 6 | type paramsMap map[string]interface{} 7 | -------------------------------------------------------------------------------- /pkg/engine/fileloader.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | //go:generate go-bindata -nometadata -pkg $GOPACKAGE -prefix ../../parts/ -o templates.go ../../parts/... 4 | //go:generate gofmt -s -l -w templates.go 5 | // fileloader use go-bindata (https://github.com/go-bindata/go-bindata) 6 | // go-bindata is the way we handle embedded files, like binary, template, etc. 7 | -------------------------------------------------------------------------------- /parts/base.t: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | {{template "params.t" .}} 6 | }, 7 | "variables": { 8 | {{template "vars.t" .}} 9 | }, 10 | "resources": [ 11 | {{template "resources.t" .}} 12 | ], 13 | "outputs": { 14 | {{template "outputs.t" .}} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/tvm-ub1804.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "TVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "tvm-ub1804", 6 | "os_type": "Linux", 7 | "os_name": "Ubuntu18.04", 8 | "vm_size": "Standard_D2s_v3", 9 | "ports": [22], 10 | "has_dns_name": false 11 | }, 12 | "linux_profile": { 13 | "admin_username": "azureuser", 14 | "ssh_public_keys": [ 15 | { 16 | "key_data": "ssh-rsa AAAA" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/cvm-win2022.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "vm_category": "CVM", 4 | "properties": { 5 | "vm_profile": { 6 | "name": "cvm-win2022", 7 | "os_image_name": "Windows Server 2019 Gen 2", 8 | "vm_size": "Standard_DC32ads_v5", 9 | "security_type": "VMGuestStateOnly", 10 | "os_disk_type": "StandardSSD_LRS", 11 | "ports": [3389], 12 | "has_dns_name": false 13 | }, 14 | 15 | "windows_profile": { 16 | "admin_username": "acctestuser", 17 | "admin_password_or_key": "Password1234!" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/cvm-win-ec-encrypted.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "vm_category": "CVM", 4 | "properties": { 5 | "vm_profile": { 6 | "name": "cvm-win2019e", 7 | "os_image_name": "Windows Server 2019 Gen 2", 8 | "vm_size": "Standard_EC2as_v5", 9 | "security_type": "DiskWithVMGuestState", 10 | "os_disk_type": "StandardSSD_LRS", 11 | "ports": [3389], 12 | "has_dns_name": false 13 | }, 14 | 15 | "windows_profile": { 16 | "admin_username": "acctestuser", 17 | "admin_password_or_key": "Password1234!" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/cvm-win2022-encrypted.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "vm_category": "CVM", 4 | "properties": { 5 | "vm_profile": { 6 | "name": "cvm-win2022e", 7 | "os_image_name": "Windows Server 2022 Gen 2", 8 | "vm_size": "Standard_DC2ads_v5", 9 | "security_type": "DiskWithVMGuestState", 10 | "os_disk_type": "StandardSSD_LRS", 11 | "ports": [3389], 12 | "has_dns_name": false 13 | }, 14 | 15 | "windows_profile": { 16 | "admin_username": "acctestuser", 17 | "admin_password_or_key": "Password1234!" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/cvm-ubuntu.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "cvm-ubuntu", 6 | "os_image_name": "Ubuntu 20.04 LTS Gen 2", 7 | "os_name": "Ubuntu20.04", 8 | "vm_size": "Standard_DC2as_v5", 9 | "ports": [22], 10 | "has_dns_name": false, 11 | "security_type": "Unencrypted" 12 | }, 13 | "linux_profile": { 14 | "authentication_type": "sshPublicKey", 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/cvm-win.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "cvm-win", 6 | "os_image_name": "Windows Server 2022 Gen 2", 7 | "vm_size": "Standard_DC2as_v5", 8 | "ports": [3389], 9 | "has_dns_name": false, 10 | "security_type": "Unencrypted", 11 | "tip_node_session_id": "af2a0623-09d8-4e43-bccd-ab6269f195d4", 12 | "cluster_name": "CBN10PrdApp04" 13 | }, 14 | "windows_profile": { 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "Password1234!" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/cvm-win2019.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "vm_category": "CVM", 4 | "properties": { 5 | "vm_profile": { 6 | "name": "cvm-win2019-dc16as", 7 | "os_image_name": "Windows Server 2019 Gen 2", 8 | "vm_size": "Standard_DC16as_v5", 9 | "security_type": "VMGuestStateOnly", 10 | "os_disk_type": "StandardSSD_LRS", 11 | "ports": [3389], 12 | "has_dns_name": false, 13 | "tip_node_session_id": "3f92ae0d-b878-43e6-b5a9-ede99ad63dee", 14 | "cluster_name": "SJC20PrdApp39" 15 | }, 16 | "windows_profile": { 17 | "admin_username": "acctestuser", 18 | "admin_password_or_key": "Password1234!" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test/cvm-win2019-encrypted.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "vm_category": "CVM", 4 | "properties": { 5 | "vm_profile": { 6 | "name": "amd-win19-4", 7 | "os_image_name": "Windows Server 2019 Gen 2", 8 | "vm_size": "Standard_DC16as_v5", 9 | "security_type": "VMGuestStateOnly", 10 | "os_disk_type": "StandardSSD_LRS", 11 | "ports": [3389], 12 | "has_dns_name": false, 13 | "tip_node_session_id": "31fbbebd-3726-4a0a-9da4-53f44329558d", 14 | "cluster_name": "SJC20PrdApp39" 15 | }, 16 | 17 | "windows_profile": { 18 | "admin_username": "acctestuser", 19 | "admin_password_or_key": "Password1234!" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: generate 3 | go build ./cmd/acc-vm-engine/ 4 | 5 | windows: generate 6 | GOOS=windows GOARCH=386 go build -o acc-vm-engine.exe ./cmd/acc-vm-engine/ 7 | 8 | generate: bootstrap 9 | GO111MODULE=on go mod tidy 10 | GO111MODULE=on go mod vendor 11 | go generate $(GOFLAGS) -v ./pkg/engine/fileloader.go 12 | 13 | .PHONY: bootstrap 14 | bootstrap: 15 | which go-bindata || go get github.com/go-bindata/go-bindata/... 16 | 17 | .PHONY: test 18 | test: 19 | ./acc-vm-engine generate -c ./test/tvm-ub1804.json -o ./_output/tvm-ub1804 20 | ./acc-vm-engine generate -c ./test/cvm-win.json -o ./_output/cvm-win 21 | 22 | .PHONY: clean 23 | clean: 24 | rm -f ./acc-vm-engine 25 | -------------------------------------------------------------------------------- /cmd/acc-vm-engine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | ) 9 | 10 | func main() { 11 | genCmd := NewGenerateCmd() 12 | 13 | gen := kingpin.Command("generate", "Generate VM template.") 14 | gen.Flag("config.file", "Configuration file path.").Short('c').Required().StringVar(&genCmd.configFile) 15 | gen.Flag("output.directory", "Output directory.").Short('o').StringVar(&genCmd.outputDir) 16 | gen.Flag("ssh.public.key", "SSH public key file path.").Short('k').StringsVar(&genCmd.sshPubKeys) 17 | 18 | switch kingpin.Parse() { 19 | case "generate": 20 | if err := genCmd.Run(); err != nil { 21 | fmt.Println("Error:", err) 22 | os.Exit(1) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/engine/ssh.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "fmt" 7 | 8 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 9 | ) 10 | 11 | // CreateSaveSSH generates and stashes an SSH key pair. 12 | func CreateSaveSSH(username, outputDirectory string) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { 13 | 14 | privateKey, publicKeyString, err = helpers.CreateSSH(rand.Reader) 15 | if err != nil { 16 | return nil, "", err 17 | } 18 | 19 | privateKeyPem := privateKeyToPem(privateKey) 20 | 21 | f := &FileSaver{} 22 | 23 | err = f.SaveFile(outputDirectory, fmt.Sprintf("%s_rsa", username), privateKeyPem) 24 | if err != nil { 25 | return nil, "", err 26 | } 27 | 28 | return privateKey, publicKeyString, nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/engine/transform/transform_test.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func ValidateTemplate(templateMap map[string]interface{}, expectedFileContents []byte, testFileName string) { 12 | output, e := helpers.JSONMarshal(templateMap, false) 13 | Expect(e).To(BeNil()) 14 | prettyOutput, e := PrettyPrintArmTemplate(string(output)) 15 | Expect(e).To(BeNil()) 16 | prettyExpectedOutput, e := PrettyPrintArmTemplate(string(expectedFileContents)) 17 | Expect(e).To(BeNil()) 18 | if prettyOutput != prettyExpectedOutput { 19 | ioutil.WriteFile(fmt.Sprintf("./transformtestfiles/%s.failure.json", testFileName), []byte(prettyOutput), 0600) 20 | } 21 | Expect(prettyOutput).To(Equal(prettyExpectedOutput)) 22 | } 23 | -------------------------------------------------------------------------------- /test/cvm-win-ubuntu.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "anjuli-measuring-cvm", 6 | "os_type": "Linux", 7 | "secure_boot_enabled":false, 8 | "os_disk": { 9 | "vhd_url": "https://premcvmcirrusbitseus.blob.core.windows.net/vhds/encrypted/ubuntu1804/maaprod/latest_ubuntu1804_1k_lsvmload_opt.vhd", 10 | "vmgs_url": "https://premcvmcirrusbitseus.blob.core.windows.net/vhds/encrypted/ubuntu1804/maaprod/latest_ubuntu1804_1k_lsvmload_opt_vmgs.vhd", 11 | "storage_account_id": "/subscriptions/504a8247-b5ff-430b-8e53-686a2b87e541/resourceGroups/mboddewy-cvm-setup/providers/Microsoft.Storage/storageAccounts/premcvmcirrusbitseus" 12 | }, 13 | "vm_size": "Standard_DC1as_v4", 14 | "ports": [22], 15 | "has_dns_name": false 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/microsoft/acc-vm-engine 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Jeffail/gabs v1.4.0 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 8 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 9 | github.com/blang/semver v3.5.1+incompatible 10 | github.com/ghodss/yaml v1.0.0 11 | github.com/go-playground/universal-translator v0.17.0 // indirect 12 | github.com/leodido/go-urn v1.2.0 // indirect 13 | github.com/onsi/gomega v1.10.3 14 | github.com/pkg/errors v0.9.1 15 | github.com/sirupsen/logrus v1.7.0 16 | github.com/stretchr/testify v1.6.1 // indirect 17 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 18 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 19 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 20 | gopkg.in/go-playground/validator.v9 v9.31.0 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/engine/filesaver.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // FileSaver represents the object that save string or byte data to file 13 | type FileSaver struct { 14 | } 15 | 16 | // SaveFileString saves string to file 17 | func (f *FileSaver) SaveFileString(dir string, file string, data string) error { 18 | return f.SaveFile(dir, file, []byte(data)) 19 | } 20 | 21 | // SaveFile saves binary data to file 22 | func (f *FileSaver) SaveFile(dir string, file string, data []byte) error { 23 | if _, err := os.Stat(dir); os.IsNotExist(err) { 24 | if e := os.MkdirAll(dir, 0700); e != nil { 25 | return fmt.Errorf("error creating directory '%s': %s", dir, e.Error()) 26 | } 27 | } 28 | 29 | path := path.Join(dir, file) 30 | if err := ioutil.WriteFile(path, []byte(data), 0600); err != nil { 31 | return err 32 | } 33 | 34 | log.Debugf("output: wrote %s", path) 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/helpers/helpers_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "testing" 4 | 5 | func TestPointerToBool(t *testing.T) { 6 | boolVar := true 7 | ret := PointerToBool(boolVar) 8 | if *ret != boolVar { 9 | t.Fatalf("expected PointerToBool(true) to return *true, instead returned %#v", ret) 10 | } 11 | } 12 | 13 | func TestIsRegionNormalized(t *testing.T) { 14 | cases := []struct { 15 | input string 16 | expectedResult string 17 | }{ 18 | { 19 | input: "westus", 20 | expectedResult: "westus", 21 | }, 22 | { 23 | input: "West US", 24 | expectedResult: "westus", 25 | }, 26 | { 27 | input: "Eastern Africa", 28 | expectedResult: "easternafrica", 29 | }, 30 | { 31 | input: "", 32 | expectedResult: "", 33 | }, 34 | } 35 | 36 | for _, c := range cases { 37 | result := NormalizeAzureRegion(c.input) 38 | if c.expectedResult != result { 39 | t.Fatalf("NormalizeAzureRegion returned unexpected result: expected %s but got %s", c.expectedResult, result) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/api/common/net.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // CidrFirstIP returns the first IP of the provided subnet. 8 | func CidrFirstIP(cidr net.IP) net.IP { 9 | for j := len(cidr) - 1; j >= 0; j-- { 10 | cidr[j]++ 11 | if cidr[j] > 0 { 12 | break 13 | } 14 | } 15 | return cidr 16 | } 17 | 18 | // CidrStringFirstIP returns the first IP of the provided subnet string. Returns an error 19 | // if the string cannot be parsed. 20 | func CidrStringFirstIP(ip string) (net.IP, error) { 21 | cidr, _, err := net.ParseCIDR(ip) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return CidrFirstIP(cidr), nil 26 | } 27 | 28 | // IP4BroadcastAddress returns the broadcast address for the given IP subnet. 29 | func IP4BroadcastAddress(n *net.IPNet) net.IP { 30 | // see https://groups.google.com/d/msg/golang-nuts/IrfXFTUavXE/8YwzIOBwJf0J 31 | ip4 := n.IP.To4() 32 | if ip4 == nil { 33 | return nil 34 | } 35 | last := make(net.IP, len(ip4)) 36 | copy(last, ip4) 37 | for i := range ip4 { 38 | last[i] |= ^n.Mask[i] 39 | } 40 | return last 41 | } 42 | -------------------------------------------------------------------------------- /pkg/engine/output.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/microsoft/acc-vm-engine/pkg/api" 5 | ) 6 | 7 | // ArtifactWriter represents the object that writes artifacts 8 | type ArtifactWriter struct { 9 | } 10 | 11 | // WriteTLSArtifacts saves TLS certificates and keys to the server filesystem 12 | func (w *ArtifactWriter) WriteTLSArtifacts(vm *api.APIModel, template, parameters, artifactsDir string, parametersOnly bool) error { 13 | if len(artifactsDir) == 0 { 14 | artifactsDir = "_output" 15 | } 16 | 17 | f := &FileSaver{} 18 | 19 | // convert back the API object, and write it 20 | var b []byte 21 | var err error 22 | if !parametersOnly { 23 | apiloader := &api.Apiloader{} 24 | b, err = apiloader.SerializeVM(vm) 25 | 26 | if err != nil { 27 | return err 28 | } 29 | 30 | if e := f.SaveFile(artifactsDir, "apimodel.json", b); e != nil { 31 | return e 32 | } 33 | 34 | if e := f.SaveFileString(artifactsDir, "azuredeploy.json", template); e != nil { 35 | return e 36 | } 37 | } 38 | 39 | if e := f.SaveFileString(artifactsDir, "azuredeploy.parameters.json", parameters); e != nil { 40 | return e 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/api/common/helper.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | 6 | validator "gopkg.in/go-playground/validator.v9" 7 | ) 8 | 9 | const ( 10 | // MinDiskSizeGB specifies the minimum attached disk size 11 | MinDiskSizeGB = 1 12 | // MaxDiskSizeGB specifies the maximum attached disk size 13 | MaxDiskSizeGB = 1023 14 | ) 15 | 16 | // HandleValidationErrors is the helper function to catch validator.ValidationError 17 | // based on Namespace of the error, and return customized error message. 18 | func HandleValidationErrors(e validator.ValidationErrors) error { 19 | err := e[0] 20 | ns := err.Namespace() 21 | switch ns { 22 | case "Properties.MasterProfile", "Properties.MasterProfile.DNSPrefix", "Properties.MasterProfile.VMSize", 23 | "Properties.LinuxProfile", 24 | "Properties.WindowsProfile.AdminUsername", 25 | "Properties.WindowsProfile.AdminPasswordOrKey": 26 | return fmt.Errorf("missing %s", ns) 27 | case "Properties.MasterProfile.OSDiskSizeGB": 28 | return fmt.Errorf("Invalid os disk size of %d specified. The range of valid values are [%d, %d]", err.Value().(int), MinDiskSizeGB, MaxDiskSizeGB) 29 | } 30 | return fmt.Errorf("Namespace %s is not caught, %+v", ns, e) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /test/cvm-linux2004.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "cvm-linux2004", 6 | "os_image_name": "Ubuntu 20.04 LTS Gen 2", 7 | "vm_size": "Standard_DC16as_v5", 8 | "os_disk_type": "StandardSSD_LRS", 9 | "security_type": "VMGuestStateOnly", 10 | "ports": [22], 11 | "has_dns_name": false, 12 | "tip_node_session_id": "61e9b5dc-a89c-43e6-990a-6b232982d378", 13 | "cluster_name": "SJC20PrdApp39" 14 | }, 15 | "linux_profile": { 16 | "authentication_type": "sshPublicKey", 17 | "admin_username": "acctestuser", 18 | "admin_password_or_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDd1MoHxSx0ZNl4O72Zauot+eGJcozCcS+eEh/sUpOyGGWKYyndPColnBgj+4HzZtrRJ/w98oAE75nszMH2xKY4a/a3Pk4d2szkA8fqnBZVgiyFFOsIhNvTzreWDpS2MEz7cMsIZEI7QqjxOrnV2HpZ+7QRNf4uNUagGVxxA82Br97lH1XOqDWzw99ylR0PQPDNYUQiJqfZxgimkpFr1sNgWaegq5Vg13f7SaQzocmbLaU4Lc8ArjFuzO8Fvl3iyB8eu2YOp3cTvEuYR3gWw7Dlmny2yPvsWoxA7t01Ml9RTUnc00mmOP9qHDwTX4RzR933ePKLbgPdsJup3ZnQtUzFSnMbJEVuMhJ9HE9z78jeGc5LMzMLEyfrpfuY4QHCtpwPYAzSIoLwYq2gec4E9xyeVXLhF4xDUGWL1wT0jkzLXwM8jAD8tthBhMPyiMo3VKaB1kI96CGQELBF/xl59fD0pzKbV0hxFRNpBq38Yff/1ZimgIC7GPQwguDWtjr+eyU= angoring@MININT-BOODHL8" 19 | } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /test/cvm-linux1804.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "anjuli-cvm-ub18", 6 | "os_image_name": "Ubuntu 18.04 LTS Gen 2", 7 | "vm_size": "Standard_DC4ads_v5", 8 | "os_disk_type": "StandardSSD_LRS", 9 | "security_type": "VMGuestStateOnly", 10 | "ports": [22], 11 | "has_dns_name": false 12 | }, 13 | "linux_profile": { 14 | "authentication_type": "sshPublicKey", 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDugVDwpAsjo1dK0UmTdjYF63RYw8lXjrlttayW6OrfcTCwDlCqGLluQJLjbA3mObZgdXilDF4Yqi1amAnFdHYWss7SwYm+W/ad8pjEi8Lq50Z95whsz0BcFBH8tlFrRb3DJ33Fv1TYiNs2vwod8iuH/+IBGB0e9dcMg603l3m7W4m2OOKnpIppbCZI1kxPCc+f6gJfiJmBnAx5bKLDt5nKqFVq9wEEeW2u6x+tKQjn6GRcK6wKF6AdQDyjzg2kWs9SmQURzode2ZQSit70v97qY7mTvPR5uKQcpls3bRkd5wYWyUQgiTo+A6aK76Q3bzVGiOi3ihX4AY7ChwLddZDnYbYTwUkCX4FNYbyy3fcfzwQgte5yL4i8/ivixSbCO3jEgfLaGtBgg0+sv/MGMUdn4OEGUYgGuQLCUsvWd5r9UPwKGxJzAbEU78py2c8poQfBWgePaHh8BrZRhL/z0qQwDapgskXPCwL3x3Nq/JHtPkXu1CRHxIVqln9G9Msp1fU21GSF9r7KMI1ECXbh/gcBN6PnXnfPVPQoeC5ykN49k+GUtUcSUGv0joZUAcg8cXHewhvt7uBH4BP8buDVuj5KSCEeE3ujkSAtxQOpaJbWZN9lLQ94DoStVge82yBkJK9h9pMPHuTviy/MPjbxi0ru9AuLYbSf2YBqtyjyxWbUw== soccerl" 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /test/cvm-linux1804-east-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "cvm-linux1804e", 6 | "os_image_name": "Ubuntu 18.04 LTS Gen 2", 7 | "vm_size": "Standard_DC2as_v5", 8 | "os_disk_type": "StandardSSD_LRS", 9 | "security_type": "VMGuestStateOnly", 10 | "ports": [22], 11 | "has_dns_name": false 12 | }, 13 | "linux_profile": { 14 | "authentication_type": "sshPublicKey", 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDugVDwpAsjo1dK0UmTdjYF63RYw8lXjrlttayW6OrfcTCwDlCqGLluQJLjbA3mObZgdXilDF4Yqi1amAnFdHYWss7SwYm+W/ad8pjEi8Lq50Z95whsz0BcFBH8tlFrRb3DJ33Fv1TYiNs2vwod8iuH/+IBGB0e9dcMg603l3m7W4m2OOKnpIppbCZI1kxPCc+f6gJfiJmBnAx5bKLDt5nKqFVq9wEEeW2u6x+tKQjn6GRcK6wKF6AdQDyjzg2kWs9SmQURzode2ZQSit70v97qY7mTvPR5uKQcpls3bRkd5wYWyUQgiTo+A6aK76Q3bzVGiOi3ihX4AY7ChwLddZDnYbYTwUkCX4FNYbyy3fcfzwQgte5yL4i8/ivixSbCO3jEgfLaGtBgg0+sv/MGMUdn4OEGUYgGuQLCUsvWd5r9UPwKGxJzAbEU78py2c8poQfBWgePaHh8BrZRhL/z0qQwDapgskXPCwL3x3Nq/JHtPkXu1CRHxIVqln9G9Msp1fU21GSF9r7KMI1ECXbh/gcBN6PnXnfPVPQoeC5ykN49k+GUtUcSUGv0joZUAcg8cXHewhvt7uBH4BP8buDVuj5KSCEeE3ujkSAtxQOpaJbWZN9lLQ94DoStVge82yBkJK9h9pMPHuTviy/MPjbxi0ru9AuLYbSf2YBqtyjyxWbUw== soccerl" 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /test/cvm-linux1804-encrypted.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "cvm-linux1804e", 6 | "os_image_name": "Ubuntu 18.04 LTS Gen 2", 7 | "vm_size": "Standard_DC2ads_v5", 8 | "os_disk_type": "StandardSSD_LRS", 9 | "security_type": "DiskWithVMGuestState", 10 | "ports": [22], 11 | "has_dns_name": false 12 | }, 13 | "linux_profile": { 14 | "authentication_type": "sshPublicKey", 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDugVDwpAsjo1dK0UmTdjYF63RYw8lXjrlttayW6OrfcTCwDlCqGLluQJLjbA3mObZgdXilDF4Yqi1amAnFdHYWss7SwYm+W/ad8pjEi8Lq50Z95whsz0BcFBH8tlFrRb3DJ33Fv1TYiNs2vwod8iuH/+IBGB0e9dcMg603l3m7W4m2OOKnpIppbCZI1kxPCc+f6gJfiJmBnAx5bKLDt5nKqFVq9wEEeW2u6x+tKQjn6GRcK6wKF6AdQDyjzg2kWs9SmQURzode2ZQSit70v97qY7mTvPR5uKQcpls3bRkd5wYWyUQgiTo+A6aK76Q3bzVGiOi3ihX4AY7ChwLddZDnYbYTwUkCX4FNYbyy3fcfzwQgte5yL4i8/ivixSbCO3jEgfLaGtBgg0+sv/MGMUdn4OEGUYgGuQLCUsvWd5r9UPwKGxJzAbEU78py2c8poQfBWgePaHh8BrZRhL/z0qQwDapgskXPCwL3x3Nq/JHtPkXu1CRHxIVqln9G9Msp1fU21GSF9r7KMI1ECXbh/gcBN6PnXnfPVPQoeC5ykN49k+GUtUcSUGv0joZUAcg8cXHewhvt7uBH4BP8buDVuj5KSCEeE3ujkSAtxQOpaJbWZN9lLQ94DoStVge82yBkJK9h9pMPHuTviy/MPjbxi0ru9AuLYbSf2YBqtyjyxWbUw== soccerl" 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /test/cvm-linux2004-encrypted.json: -------------------------------------------------------------------------------- 1 | { 2 | "vm_category": "CVM", 3 | "properties": { 4 | "vm_profile": { 5 | "name": "anjuli-testing", 6 | "os_image_name": "Ubuntu 20.04 LTS Gen 2 TVMCVM", 7 | "vm_size": "Standard_DC2as_v5", 8 | "os_disk_type": "StandardSSD_LRS", 9 | "security_type": "DiskWithVMGuestState", 10 | "ports": [22], 11 | "has_dns_name": false 12 | }, 13 | "linux_profile": { 14 | "authentication_type": "sshPublicKey", 15 | "admin_username": "acctestuser", 16 | "admin_password_or_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDugVDwpAsjo1dK0UmTdjYF63RYw8lXjrlttayW6OrfcTCwDlCqGLluQJLjbA3mObZgdXilDF4Yqi1amAnFdHYWss7SwYm+W/ad8pjEi8Lq50Z95whsz0BcFBH8tlFrRb3DJ33Fv1TYiNs2vwod8iuH/+IBGB0e9dcMg603l3m7W4m2OOKnpIppbCZI1kxPCc+f6gJfiJmBnAx5bKLDt5nKqFVq9wEEeW2u6x+tKQjn6GRcK6wKF6AdQDyjzg2kWs9SmQURzode2ZQSit70v97qY7mTvPR5uKQcpls3bRkd5wYWyUQgiTo+A6aK76Q3bzVGiOi3ihX4AY7ChwLddZDnYbYTwUkCX4FNYbyy3fcfzwQgte5yL4i8/ivixSbCO3jEgfLaGtBgg0+sv/MGMUdn4OEGUYgGuQLCUsvWd5r9UPwKGxJzAbEU78py2c8poQfBWgePaHh8BrZRhL/z0qQwDapgskXPCwL3x3Nq/JHtPkXu1CRHxIVqln9G9Msp1fU21GSF9r7KMI1ECXbh/gcBN6PnXnfPVPQoeC5ykN49k+GUtUcSUGv0joZUAcg8cXHewhvt7uBH4BP8buDVuj5KSCEeE3ujkSAtxQOpaJbWZN9lLQ94DoStVge82yBkJK9h9pMPHuTviy/MPjbxi0ru9AuLYbSf2YBqtyjyxWbUw== soccerl" 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /pkg/api/common/net_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | type cidrTest struct { 9 | cidr string 10 | expected string 11 | } 12 | 13 | type vnetSubnetIDTest struct { 14 | vnetSubnetID string 15 | expectedSubID string 16 | expectedRG string 17 | expectedVnet string 18 | expectedSubnet string 19 | } 20 | 21 | func Test_CidrFirstIP(t *testing.T) { 22 | scenarios := []cidrTest{ 23 | { 24 | cidr: "10.0.0.0/16", 25 | expected: "10.0.0.1", 26 | }, 27 | { 28 | cidr: "10.16.32.32/27", 29 | expected: "10.16.32.33", 30 | }, 31 | } 32 | 33 | for _, scenario := range scenarios { 34 | if first, _ := CidrStringFirstIP(scenario.cidr); first.String() != scenario.expected { 35 | t.Errorf("expected first ip of subnet %v to be %v but was %v", scenario.cidr, scenario.expected, first) 36 | } 37 | } 38 | } 39 | 40 | func Test_IP4BroadcastAddress(t *testing.T) { 41 | scenarios := []cidrTest{ 42 | { 43 | cidr: "10.0.0.0/16", 44 | expected: "10.0.255.255", 45 | }, 46 | { 47 | cidr: "10.16.32.32/27", 48 | expected: "10.16.32.63", 49 | }, 50 | } 51 | 52 | for _, scenario := range scenarios { 53 | _, cidr, _ := net.ParseCIDR(scenario.cidr) 54 | if broadcast := IP4BroadcastAddress(cidr); broadcast.String() != scenario.expected { 55 | t.Errorf("expected broadcast ip of subnet %v to be %v but was %v", scenario.cidr, scenario.expected, broadcast) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/api/const.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // DefaultGeneratorCode specifies the source generator of the cluster template. 10 | DefaultGeneratorCode = "acc-vm-engine" 11 | // DefaultVnet specifies default vnet address space 12 | DefaultVnet = "10.1.16.0/24" 13 | // DefaultSubnet specifies default subnet 14 | DefaultSubnet = "10.1.16.0/24" 15 | ) 16 | 17 | const ( 18 | // TVM VM category 19 | TVM VMCategory = "TVM" 20 | // CVM VM category 21 | CVM VMCategory = "CVM" 22 | ) 23 | 24 | const ( 25 | Linux OSType = "Linux" 26 | Windows OSType = "Windows" 27 | 28 | Ubuntu1804 OSName = "Ubuntu18.04" 29 | Ubuntu2004 OSName = "Ubuntu20.04" 30 | Windows10 OSName = "Windows10" 31 | WindowsServer2016 OSName = "WindowsServer2016" 32 | WindowsServer2019 OSName = "WindowsServer2019" 33 | Windows10SecuredCore OSName = "Windows10-SecuredCore" 34 | WindowsServer2016SecuredCore OSName = "WindowsServer2016-SecuredCore" 35 | WindowsServer2019SecuredCore OSName = "WindowsServer2019-SecuredCore" 36 | ) 37 | 38 | func getAllowedValues(vals []string) string { 39 | strFormat := `"allowedValues": [ 40 | "%s" 41 | ], 42 | ` 43 | return fmt.Sprintf(strFormat, strings.Join(vals, "\",\n \"")) 44 | } 45 | 46 | func getDefaultValue(def string) string { 47 | strFormat := `"defaultValue": "%s", 48 | ` 49 | return fmt.Sprintf(strFormat, def) 50 | } 51 | 52 | func getAllowedDefaultValues(vals []string, def string) string { 53 | return getAllowedValues(vals) + " " + getDefaultValue(def) 54 | } 55 | 56 | // GetVMSizes returns allowed and default sizes for VM 57 | func GetVMSizes(vmconf VMConfigurator) string { 58 | return getAllowedDefaultValues(vmconf.AllowedVMSizes(), vmconf.DefaultVMSize()) 59 | } 60 | 61 | // GetOsDiskTypes returns allowed and default OS disk types 62 | func GetOsDiskTypes(vmconf VMConfigurator) string { 63 | return getAllowedDefaultValues(vmconf.AllowedOsDiskTypes(), vmconf.DefaultOsDiskType()) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/api/strictjson.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func checkJSONKeys(data []byte, types ...reflect.Type) error { 11 | var raw interface{} 12 | if e := json.Unmarshal(data, &raw); e != nil { 13 | return e 14 | } 15 | o := raw.(map[string]interface{}) 16 | return checkMapKeys(o, types...) 17 | } 18 | 19 | func checkMapKeys(o map[string]interface{}, types ...reflect.Type) error { 20 | fieldMap := createJSONFieldMap(types) 21 | for k, v := range o { 22 | f, present := fieldMap[strings.ToLower(k)] 23 | if !present { 24 | return fmt.Errorf("Unknown JSON tag %s", k) 25 | } 26 | if f.Type.Kind() == reflect.Struct && v != nil { 27 | if childMap, exists := v.(map[string]interface{}); exists { 28 | if e := checkMapKeys(childMap, f.Type); e != nil { 29 | return e 30 | } 31 | } 32 | } 33 | if f.Type.Kind() == reflect.Slice && v != nil { 34 | elementType := f.Type.Elem() 35 | if elementType.Kind() == reflect.Ptr { 36 | elementType = elementType.Elem() 37 | } 38 | if childSlice, exists := v.([]interface{}); exists { 39 | for _, child := range childSlice { 40 | if childMap, exists := child.(map[string]interface{}); exists { 41 | if e := checkMapKeys(childMap, elementType); e != nil { 42 | return e 43 | } 44 | } 45 | } 46 | } 47 | } 48 | if f.Type.Kind() == reflect.Ptr && v != nil { 49 | elementType := f.Type.Elem() 50 | if childMap, exists := v.(map[string]interface{}); exists { 51 | if e := checkMapKeys(childMap, elementType); e != nil { 52 | return e 53 | } 54 | } 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | func createJSONFieldMap(types []reflect.Type) map[string]reflect.StructField { 61 | fieldMap := make(map[string]reflect.StructField) 62 | // Combine the permitted JSON fields from all types - handles the case 63 | // where we need to see the same JSON as a TypeMeta and a ContainerService 64 | for _, t := range types { 65 | for i := 0; i < t.NumField(); i++ { 66 | f := t.Field(i) 67 | tag := f.Tag 68 | fieldJSON := tag.Get("json") 69 | if fieldJSON != "" { 70 | fieldJSONkey := strings.SplitN(fieldJSON, ",", 2)[0] 71 | fieldMap[strings.ToLower(fieldJSONkey)] = f 72 | } 73 | } 74 | } 75 | return fieldMap 76 | } 77 | -------------------------------------------------------------------------------- /pkg/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rsa" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | const ( 15 | // SSHKeySize is the size (in bytes) of SSH key to create 16 | SSHKeySize = 4096 17 | ) 18 | 19 | // NormalizeAzureRegion returns a normalized Azure region with white spaces removed and converted to lower case 20 | func NormalizeAzureRegion(name string) string { 21 | return strings.ToLower(strings.Replace(name, " ", "", -1)) 22 | } 23 | 24 | // JSONMarshalIndent marshals formatted JSON w/ optional SetEscapeHTML 25 | func JSONMarshalIndent(content interface{}, prefix, indent string, escape bool) ([]byte, error) { 26 | b, err := JSONMarshal(content, escape) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var bufIndent bytes.Buffer 32 | if err := json.Indent(&bufIndent, b, prefix, indent); err != nil { 33 | return nil, err 34 | } 35 | 36 | return bufIndent.Bytes(), nil 37 | } 38 | 39 | // JSONMarshal marshals JSON w/ optional SetEscapeHTML 40 | func JSONMarshal(content interface{}, escape bool) ([]byte, error) { 41 | var buf bytes.Buffer 42 | enc := json.NewEncoder(&buf) 43 | enc.SetEscapeHTML(escape) 44 | if err := enc.Encode(content); err != nil { 45 | return nil, err 46 | } 47 | 48 | return buf.Bytes(), nil 49 | } 50 | 51 | // IsTrueBoolPointer is a simple boolean helper function for boolean pointers 52 | func IsTrueBoolPointer(b *bool) bool { 53 | if b != nil && *b { 54 | return true 55 | } 56 | return false 57 | } 58 | 59 | // PointerToBool returns a pointer to a bool 60 | func PointerToBool(b bool) *bool { 61 | p := b 62 | return &p 63 | } 64 | 65 | // CreateSSH creates an SSH key pair. 66 | func CreateSSH(rg io.Reader) (privateKey *rsa.PrivateKey, publicKeyString string, err error) { 67 | privateKey, err = rsa.GenerateKey(rg, SSHKeySize) 68 | if err != nil { 69 | return nil, "", fmt.Errorf("failed to generate private key for ssh: %q", err) 70 | } 71 | 72 | publicKey := privateKey.PublicKey 73 | sshPublicKey, err := ssh.NewPublicKey(&publicKey) 74 | if err != nil { 75 | return nil, "", fmt.Errorf("failed to create openssh public key string: %q", err) 76 | } 77 | authorizedKeyBytes := ssh.MarshalAuthorizedKey(sshPublicKey) 78 | authorizedKey := string(authorizedKeyBytes) 79 | 80 | return privateKey, authorizedKey, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/apiloader.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "reflect" 8 | 9 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 10 | ) 11 | 12 | // Apiloader represents the object that loads api model 13 | type Apiloader struct { 14 | } 15 | 16 | // VMConfigurator manages VM specific configuration 17 | type VMConfigurator interface { 18 | DefaultVMName() string 19 | OSImage() *OSImage 20 | OSImageName() string 21 | SecurityType() string 22 | DefaultOsDiskType() string 23 | AllowedOsDiskTypes() []string 24 | AllowedVMSizes() []string 25 | DefaultVMSize() string 26 | } 27 | 28 | // LoadVMFromFile loads an API Model from a JSON file 29 | func (a *Apiloader) LoadVMFromFile(jsonFile string, validate, isUpdate bool, sshPubKeys []string) (*APIModel, error) { 30 | contents, err := ioutil.ReadFile(jsonFile) 31 | if err != nil { 32 | return nil, fmt.Errorf("error reading file %s: %s", jsonFile, err.Error()) 33 | } 34 | return a.LoadVM(contents, validate, isUpdate, sshPubKeys) 35 | } 36 | 37 | // LoadVM loads and validates an API Model 38 | func (a *Apiloader) LoadVM(contents []byte, validate, isUpdate bool, sshPubKeys []string) (*APIModel, error) { 39 | vm := &APIModel{} 40 | err := json.Unmarshal(contents, vm) 41 | if err != nil { 42 | return nil, err 43 | } 44 | err = checkJSONKeys(contents, reflect.TypeOf(*vm)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | var osName OSName 49 | if vm.Properties != nil && len(vm.Properties.VMProfile.OSName) > 0 { 50 | osName = vm.Properties.VMProfile.OSName 51 | } 52 | if vm.VMConfigurator, err = getVMConfigurator(vm.VMCategory, osName); err != nil { 53 | return nil, err 54 | } 55 | // add SSH public keys from command line arguments 56 | if vm.Properties.LinuxProfile != nil { 57 | for _, key := range sshPubKeys { 58 | vm.Properties.LinuxProfile.SSHPubKeys = append(vm.Properties.LinuxProfile.SSHPubKeys, &PublicKey{KeyData: key}) 59 | } 60 | } 61 | if err := vm.Properties.Validate(vm.VMConfigurator, isUpdate); validate && err != nil { 62 | return nil, err 63 | } 64 | return vm, nil 65 | } 66 | 67 | // SerializeVM takes an unversioned container service and returns the bytes 68 | func (a *Apiloader) SerializeVM(vm *APIModel) ([]byte, error) { 69 | return helpers.JSONMarshalIndent(vm, "", " ", false) 70 | } 71 | 72 | func getVMConfigurator(vmcat VMCategory, osName OSName) (VMConfigurator, error) { 73 | switch vmcat { 74 | case TVM: 75 | return NewTVMConfigurator(osName) 76 | case CVM: 77 | return NewCVMConfigurator() 78 | default: 79 | return nil, fmt.Errorf("unsupported VM category %q", vmcat) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/cvm.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // cvmConfigurator implements VMConfigurator interface 8 | type cvmConfigurator struct{} 9 | 10 | // NewCVMConfigurator returns VMConfigurator for CVM 11 | func NewCVMConfigurator() (VMConfigurator, error) { 12 | return &cvmConfigurator{}, nil 13 | } 14 | 15 | func (h *cvmConfigurator) DefaultVMName() string { 16 | return "cvm" 17 | } 18 | 19 | // DefaultLinuxImage returns default Linux OS image 20 | func (h *cvmConfigurator) OSImage() *OSImage { 21 | log.Fatal("OSName is not set") 22 | return nil 23 | } 24 | 25 | // DefaultLinuxImageName returns default Linux OS image name 26 | func (h *cvmConfigurator) OSImageName() string { 27 | log.Info("OSImageName is not set") 28 | return "Ubuntu 20.04 LTS Gen 2" 29 | } 30 | 31 | // DefaultLinuxSecurityType returns default Linux OS security type 32 | func (h *cvmConfigurator) SecurityType() string { 33 | log.Info("SecurityType is not set") 34 | return "Unencrypted" 35 | } 36 | 37 | // DefaultOsDiskType returns default OS disk type 38 | func (h *cvmConfigurator) DefaultOsDiskType() string { 39 | return "Premium_LRS" 40 | } 41 | 42 | // AllowedOsDiskTypes returns supported OS disk types 43 | func (h *cvmConfigurator) AllowedOsDiskTypes() []string { 44 | return []string{ 45 | "Premium_LRS", 46 | "StandardSSD_LRS", 47 | "Standard_LRS", 48 | } 49 | } 50 | 51 | // AllowedVMSizes returns supported VM sizes 52 | func (h *cvmConfigurator) AllowedVMSizes() []string { 53 | return []string{ 54 | "Standard_DC2as_v5", 55 | "Standard_DC4as_v5", 56 | "Standard_DC8as_v5", 57 | "Standard_DC16as_v5", 58 | "Standard_DC32as_v5", 59 | "Standard_DC48as_v5", 60 | "Standard_DC64as_v5", 61 | "Standard_DC96as_v5", 62 | "Standard_DC2ads_v5", 63 | "Standard_DC4ads_v5", 64 | "Standard_DC8ads_v5", 65 | "Standard_DC16ads_v5", 66 | "Standard_DC32ads_v5", 67 | "Standard_DC48ads_v5", 68 | "Standard_DC64ads_v5", 69 | "Standard_DC96ads_v5", 70 | "Standard_EC2as_v5", 71 | "Standard_EC4as_v5", 72 | "Standard_EC8as_v5", 73 | "Standard_EC16as_v5", 74 | "Standard_EC20as_v5", 75 | "Standard_EC32as_v5", 76 | "Standard_EC48as_v5", 77 | "Standard_EC64as_v5", 78 | "Standard_EC96as_v5", 79 | "Standard_EC96ias_v5", 80 | "Standard_EC2ads_v5", 81 | "Standard_EC4ads_v5", 82 | "Standard_EC8ads_v5", 83 | "Standard_EC16ads_v5", 84 | "Standard_EC20ads_v5", 85 | "Standard_EC32ads_v5", 86 | "Standard_EC48ads_v5", 87 | "Standard_EC64ads_v5", 88 | "Standard_EC96ads_v5", 89 | "Standard_EC96iads_v5", 90 | } 91 | } 92 | 93 | func (h *cvmConfigurator) DefaultVMSize() string { 94 | return "Standard_DC2as_v5" 95 | } 96 | -------------------------------------------------------------------------------- /pkg/engine/transform/json.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 8 | ) 9 | 10 | // PrettyPrintArmTemplate will pretty print the arm template ensuring ordered by params, vars, resources, and outputs 11 | func PrettyPrintArmTemplate(template string) (string, error) { 12 | translateParams := [][]string{ 13 | {"\"parameters\"", "\"dparameters\""}, 14 | {"\"variables\"", "\"evariables\""}, 15 | {"\"resources\"", "\"fresources\""}, 16 | {"\"outputs\"", "\"zoutputs\""}, 17 | // there is a bug in ARM where it doesn't correctly translate back '\u003e' (>) 18 | {">", "GREATERTHAN"}, 19 | {"<", "LESSTHAN"}, 20 | {"&", "AMPERSAND"}, 21 | } 22 | 23 | template = translateJSON(template, translateParams, false) 24 | var err error 25 | if template, err = PrettyPrintJSON(template); err != nil { 26 | return "", err 27 | } 28 | template = translateJSON(template, translateParams, true) 29 | 30 | return template, nil 31 | } 32 | 33 | // PrettyPrintJSON will pretty print the json into 34 | func PrettyPrintJSON(content string) (string, error) { 35 | var data map[string]interface{} 36 | // fmt.Printf("content = %s\n", content); 37 | 38 | if err := json.Unmarshal([]byte(content), &data); err != nil { 39 | return "", err 40 | } 41 | prettyprint, err := helpers.JSONMarshalIndent(data, "", " ", false) 42 | if err != nil { 43 | return "", err 44 | } 45 | return string(prettyprint), nil 46 | } 47 | 48 | // BuildAzureParametersFile will add the correct schema and contentversion information 49 | func BuildAzureParametersFile(content string) (string, error) { 50 | var parametersMap map[string]interface{} 51 | if err := json.Unmarshal([]byte(content), ¶metersMap); err != nil { 52 | return "", err 53 | } 54 | parametersAll := map[string]interface{}{} 55 | parametersAll["$schema"] = "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" 56 | parametersAll["contentVersion"] = "1.0.0.0" 57 | parametersAll["parameters"] = parametersMap 58 | 59 | prettyprint, err := helpers.JSONMarshalIndent(parametersAll, "", " ", false) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | return string(prettyprint), nil 65 | } 66 | 67 | func translateJSON(content string, translateParams [][]string, reverseTranslate bool) string { 68 | for _, tuple := range translateParams { 69 | if len(tuple) != 2 { 70 | panic("string tuples must be of size 2") 71 | } 72 | a := tuple[0] 73 | b := tuple[1] 74 | if reverseTranslate { 75 | content = strings.Replace(content, b, a, -1) 76 | } else { 77 | content = strings.Replace(content, a, b, -1) 78 | } 79 | } 80 | return content 81 | } 82 | -------------------------------------------------------------------------------- /pkg/engine/defaults.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/microsoft/acc-vm-engine/pkg/api" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // setPropertiesDefaults for the container Properties 14 | func setPropertiesDefaults(vm *api.APIModel) { 15 | if len(vm.Properties.VMProfile.Name) == 0 { 16 | log.Warnf("Missing VM Name. Setting to %s", vm.VMConfigurator.DefaultVMName()) 17 | vm.Properties.VMProfile.Name = vm.VMConfigurator.DefaultVMName() 18 | } 19 | // set network defaults 20 | if vm.Properties.VnetProfile == nil { 21 | vm.Properties.VnetProfile = &api.VnetProfile{} 22 | } 23 | if !vm.Properties.VnetProfile.IsCustomVNET() { 24 | if len(vm.Properties.VnetProfile.VnetAddress) == 0 { 25 | vm.Properties.VnetProfile.VnetAddress = api.DefaultVnet 26 | } 27 | if len(vm.Properties.VnetProfile.SubnetAddress) == 0 { 28 | vm.Properties.VnetProfile.SubnetAddress = api.DefaultSubnet 29 | } 30 | } 31 | if len(vm.Properties.VMProfile.OSDiskType) == 0 { 32 | vm.Properties.VMProfile.OSDiskType = vm.VMConfigurator.DefaultOsDiskType() 33 | } 34 | if(vm.Properties.VMProfile.SecurityProfile == nil) { 35 | vm.Properties.VMProfile.SecurityProfile = &api.SecurityProfile{ "true","true"} 36 | } else { 37 | if len(vm.Properties.VMProfile.SecurityProfile.SecureBoot) == 0 { 38 | vm.Properties.VMProfile.SecurityProfile.SecureBoot = "true" 39 | } 40 | if len(vm.Properties.VMProfile.SecurityProfile.VTPM) == 0 { 41 | vm.Properties.VMProfile.SecurityProfile.VTPM = "true" 42 | } 43 | if (vm.Properties.VMProfile.SecurityProfile.SecureBoot == "none") { 44 | vm.Properties.VMProfile.SecurityProfile = nil 45 | } 46 | } 47 | } 48 | 49 | func combineValues(inputs ...string) string { 50 | valueMap := make(map[string]string) 51 | for _, input := range inputs { 52 | applyValueStringToMap(valueMap, input) 53 | } 54 | return mapToString(valueMap) 55 | } 56 | 57 | func applyValueStringToMap(valueMap map[string]string, input string) { 58 | values := strings.Split(input, ",") 59 | for index := 0; index < len(values); index++ { 60 | // trim spaces (e.g. if the input was "foo=true, bar=true" - we want to drop the space after the comma) 61 | value := strings.Trim(values[index], " ") 62 | valueParts := strings.Split(value, "=") 63 | if len(valueParts) == 2 { 64 | valueMap[valueParts[0]] = valueParts[1] 65 | } 66 | } 67 | } 68 | 69 | func mapToString(valueMap map[string]string) string { 70 | // Order by key for consistency 71 | keys := []string{} 72 | for key := range valueMap { 73 | keys = append(keys, key) 74 | } 75 | sort.Strings(keys) 76 | var buf bytes.Buffer 77 | for _, key := range keys { 78 | buf.WriteString(fmt.Sprintf("%s=%s,", key, valueMap[key])) 79 | } 80 | return strings.TrimSuffix(buf.String(), ",") 81 | } 82 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /cmd/acc-vm-engine/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/microsoft/acc-vm-engine/pkg/api" 11 | "github.com/microsoft/acc-vm-engine/pkg/engine" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type generateCmd struct { 16 | configFile string 17 | outputDir string 18 | sshPubKeys []string 19 | 20 | // derived 21 | vm *api.APIModel 22 | } 23 | 24 | func NewGenerateCmd() *generateCmd { 25 | return &generateCmd{ 26 | sshPubKeys: []string{}, 27 | } 28 | } 29 | 30 | func (h *generateCmd) Run() error { 31 | if err := h.validate(); err != nil { 32 | return errors.Wrap(err, "failed to validate 'generate'") 33 | } 34 | 35 | if err := h.loadAPIModel(); err != nil { 36 | return errors.Wrap(err, "failed to load API model in 'generate'") 37 | } 38 | return h.run() 39 | } 40 | 41 | func (h *generateCmd) validate() error { 42 | if _, err := os.Stat(h.configFile); os.IsNotExist(err) { 43 | return err 44 | } 45 | for i, keyPath := range h.sshPubKeys { 46 | if _, err := os.Stat(keyPath); os.IsNotExist(err) { 47 | return err 48 | } 49 | b, err := ioutil.ReadFile(keyPath) 50 | if err != nil { 51 | return err 52 | } 53 | h.sshPubKeys[i] = strings.TrimSpace(string(b)) 54 | } 55 | return nil 56 | } 57 | 58 | func (h *generateCmd) loadAPIModel() error { 59 | var err error 60 | 61 | apiloader := &api.Apiloader{} 62 | h.vm, err = apiloader.LoadVMFromFile(h.configFile, true, false, h.sshPubKeys) 63 | if err != nil { 64 | return errors.Wrap(err, "failed to parse config file") 65 | } 66 | 67 | if h.outputDir == "" { 68 | h.outputDir = "_output" 69 | } 70 | return nil 71 | } 72 | 73 | func (h *generateCmd) run() error { 74 | fmt.Printf("Generating assets into %s...\n", h.outputDir) 75 | 76 | templateGenerator, err := engine.InitializeTemplateGenerator(h.vm.VMConfigurator) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | template, parameters, err := templateGenerator.GenerateTemplate(h.vm, api.DefaultGeneratorCode) 82 | if err != nil { 83 | log.Fatalf("error generating template %s: %s", h.configFile, err.Error()) 84 | os.Exit(1) 85 | } 86 | /* 87 | if !gc.noPrettyPrint { 88 | if template, err = transform.PrettyPrintArmTemplate(template); err != nil { 89 | log.Fatalf("error pretty printing template: %s \n", err.Error()) 90 | } 91 | if parameters, err = transform.BuildAzureParametersFile(parameters); err != nil { 92 | log.Fatalf("error pretty printing template parameters: %s \n", err.Error()) 93 | } 94 | } 95 | */ 96 | writer := &engine.ArtifactWriter{} 97 | if err = writer.WriteTLSArtifacts(h.vm, template, parameters, h.outputDir, false); err != nil { 98 | log.Fatalf("error writing artifacts: %s \n", err.Error()) 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/engine/pki_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "testing" 7 | ) 8 | 9 | func TestCreateCertificateWithOrganisation(t *testing.T) { 10 | var err error 11 | var caPair *PkiKeyCertPair 12 | 13 | var ( 14 | caCertificate *x509.Certificate 15 | caPrivateKey *rsa.PrivateKey 16 | testCertificate *x509.Certificate 17 | ) 18 | 19 | caCertificate, caPrivateKey, err = createCertificate("ca", nil, nil, false, false, nil, nil, nil) 20 | if err != nil { 21 | t.Fatalf("failed to generate certificate: %s", err) 22 | } 23 | caPair = &PkiKeyCertPair{CertificatePem: string(certificateToPem(caCertificate.Raw)), PrivateKeyPem: string(privateKeyToPem(caPrivateKey))} 24 | 25 | caCertificate, err = pemToCertificate(caPair.CertificatePem) 26 | if err != nil { 27 | t.Fatalf("failed to generate certificate: %s", err) 28 | } 29 | caPrivateKey, err = pemToKey(caPair.PrivateKeyPem) 30 | if err != nil { 31 | t.Fatalf("failed to generate certificate: %s", err) 32 | } 33 | 34 | organization := make([]string, 1) 35 | organization[0] = "system:masters" 36 | testCertificate, _, err = createCertificate("client", caCertificate, caPrivateKey, false, false, nil, nil, organization) 37 | if err != nil { 38 | t.Fatalf("failed to generate certificate: %s", err) 39 | } 40 | 41 | certificationOrganization := testCertificate.Subject.Organization 42 | 43 | if certificationOrganization[0] != organization[0] || len(certificationOrganization) != len(organization) { 44 | t.Fatalf("certificate organisation did not match") 45 | } 46 | } 47 | 48 | func TestCreateCertificateWithoutOrganisation(t *testing.T) { 49 | var err error 50 | var caPair *PkiKeyCertPair 51 | 52 | var ( 53 | caCertificate *x509.Certificate 54 | caPrivateKey *rsa.PrivateKey 55 | testCertificate *x509.Certificate 56 | ) 57 | 58 | caCertificate, caPrivateKey, err = createCertificate("ca", nil, nil, false, false, nil, nil, nil) 59 | if err != nil { 60 | t.Fatalf("failed to generate certificate: %s", err) 61 | } 62 | caPair = &PkiKeyCertPair{CertificatePem: string(certificateToPem(caCertificate.Raw)), PrivateKeyPem: string(privateKeyToPem(caPrivateKey))} 63 | 64 | caCertificate, err = pemToCertificate(caPair.CertificatePem) 65 | if err != nil { 66 | t.Fatalf("failed to generate certificate: %s", err) 67 | } 68 | caPrivateKey, err = pemToKey(caPair.PrivateKeyPem) 69 | if err != nil { 70 | t.Fatalf("failed to generate certificate: %s", err) 71 | } 72 | 73 | testCertificate, _, err = createCertificate("client", caCertificate, caPrivateKey, false, false, nil, nil, nil) 74 | if err != nil { 75 | t.Fatalf("failed to generate certificate: %s", err) 76 | } 77 | 78 | certificationOrganization := testCertificate.Subject.Organization 79 | 80 | if len(certificationOrganization) != 0 { 81 | t.Fatalf("certificate organisation should be empty but has length %d", len(certificationOrganization)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/engine/params.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | 5 | "github.com/microsoft/acc-vm-engine/pkg/api" 6 | ) 7 | 8 | func getParameters(vm *api.APIModel, generatorCode string) (paramsMap, error) { 9 | properties := vm.Properties 10 | parametersMap := paramsMap{} 11 | 12 | addValue(parametersMap, "vmName", properties.VMProfile.Name) 13 | addValue(parametersMap, "vmSize", properties.VMProfile.VMSize) 14 | addValue(parametersMap, "osImageName", properties.VMProfile.OSImageName) 15 | addValue(parametersMap, "securityType", properties.VMProfile.SecurityType) 16 | if len(properties.VMProfile.OSDiskType) > 0 { 17 | addValue(parametersMap, "osDiskType", properties.VMProfile.OSDiskType) 18 | } 19 | if properties.LinuxProfile != nil { 20 | addValue(parametersMap, "adminUsername", properties.LinuxProfile.AdminUsername) 21 | if properties.LinuxProfile.AuthenticationType =="password" { 22 | addValue(parametersMap, "authenticationType", "password") 23 | addValue(parametersMap, "adminPasswordOrKey", properties.LinuxProfile.AdminPasswordOrKey) 24 | } else { 25 | addValue(parametersMap, "authenticationType", "sshPublicKey") 26 | addValue(parametersMap, "adminPasswordOrKey", properties.LinuxProfile.AdminPasswordOrKey) 27 | } 28 | } 29 | if properties.WindowsProfile != nil { 30 | addValue(parametersMap, "adminUsername", properties.WindowsProfile.AdminUsername) 31 | addValue(parametersMap, "adminPasswordOrKey", properties.WindowsProfile.AdminPasswordOrKey) 32 | } 33 | if properties.VMProfile.SecurityProfile != nil { 34 | addValue(parametersMap, "secureBootEnabled", properties.VMProfile.SecurityProfile.SecureBoot) 35 | addValue(parametersMap, "vTPMEnabled", properties.VMProfile.SecurityProfile.VTPM) 36 | } 37 | if len(properties.VMProfile.TipNodeSessionID) > 0 { 38 | addValue(parametersMap, "tipNodeSessionId", properties.VMProfile.TipNodeSessionID) 39 | } 40 | if len(properties.VMProfile.ClusterName) > 0 { 41 | addValue(parametersMap, "clusterName", properties.VMProfile.ClusterName) 42 | } 43 | if properties.VnetProfile.IsCustomVNET() { 44 | addValue(parametersMap, "vnetNewOrExisting", "existing") 45 | addValue(parametersMap, "vnetResourceGroupName", properties.VnetProfile.VnetResourceGroup) 46 | addValue(parametersMap, "virtualNetworkName", properties.VnetProfile.VirtualNetworkName) 47 | addValue(parametersMap, "subnetName", properties.VnetProfile.SubnetName) 48 | } else { 49 | addValue(parametersMap, "vnetNewOrExisting", "new") 50 | addValue(parametersMap, "subnetAddress", properties.VnetProfile.SubnetAddress) 51 | } 52 | if properties.DiagnosticsProfile != nil && properties.DiagnosticsProfile.Enabled { 53 | addValue(parametersMap, "bootDiagnostics", "true") 54 | addValue(parametersMap, "diagnosticsStorageAccountName", properties.DiagnosticsProfile.StorageAccountName) 55 | if properties.DiagnosticsProfile.IsNewStorageAccount { 56 | addValue(parametersMap, "diagnosticsStorageAccountNewOrExisting", "new") 57 | } else { 58 | addValue(parametersMap, "diagnosticsStorageAccountNewOrExisting", "existing") 59 | } 60 | } else { 61 | addValue(parametersMap, "bootDiagnostics", "false") 62 | } 63 | return parametersMap, nil 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure template generator for ACC VMs 2 | 3 | ### Supported VMs 4 | - Confidential VM (CVM) 5 | 6 | #### Pre-Requisite 7 | If you want to build on Linux or MacOS, make sure you have golang installed. If not, you can download it by following these instructions: 8 | ```sh 9 | # update your system 10 | sudo apt update 11 | sudo apt upgrade 12 | 13 | # download the go binary 14 | wget https://dl.google.com/go/go1.15.5.linux-amd64.tar.gz 15 | 16 | # extract binaries tarball 17 | sudo tar -C /usr/local/ -xzf go1.15.5.linux-amd64.tar.gz 18 | 19 | # set the right path 20 | cd /usr/local/ 21 | echo $PATH 22 | sudo nano $HOME/.profile 23 | ``` 24 | inside your profile, append the following: 25 | ```sh 26 | export PATH=$PATH:/usr/local/go/bin 27 | ``` 28 | save and apply changes: 29 | ```sh 30 | source $HOME/.profile 31 | ``` 32 | check that it installed correctly: 33 | ```sh 34 | go version 35 | ``` 36 | 37 | #### Build 38 | On Linux or MacOS, execute: 39 | 40 | ```sh 41 | make build 42 | ``` 43 | 44 | Alternatively, you can build with a Docker container: 45 | ```sh 46 | docker run \ 47 | -v $PWD:/go/src/github.com/microsoft/acc-vm-engine \ 48 | -w /go/src/github.com/microsoft/acc-vm-engine \ 49 | golang:1.15-alpine go build ./cmd/acc-vm-engine 50 | ``` 51 | 52 | #### Generate template 53 | A sample configuration file for CVM deployment is located in the [test/](./test/) folder. 54 | If your machine runs Linux or MacOS, you can execute the following command to generate templates for a windows vm deployment: 55 | ```sh 56 | ./acc-vm-engine generate -c ./test/cvm-win.json 57 | ``` 58 | 59 | Please note that for a linux deployment, you will have to put your ssh public key into the [test/cvm-ubuntu.json](./test/cvm-ubuntu.json) parameter file under 'admin_password_or_key' or you will not be able to connect. Then execute: 60 | ```sh 61 | ./acc-vm-engine generate -c ./test/cvm-ubuntu.json 62 | ``` 63 | 64 | In case you need to generate an ssh public key, you can use the following command and use the public key content that it outputs into your ~/.ssh folder: 65 | ``` 66 | ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 67 | ``` 68 | 69 | Alternatively, use Docker container: 70 | ```sh 71 | docker run \ 72 | -v $PWD:/go/src/github.com/microsoft/acc-vm-engine \ 73 | -w /go/src/github.com/microsoft/acc-vm-engine \ 74 | golang:1.15-alpine ./acc-vm-engine generate -c ./test/cvm-win.json 75 | ``` 76 | The template and parameter files will be generated in `_output` directory (by default). 77 | 78 | #### Deploy the VM 79 | Use [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) or PowerShell to deploy the VM. 80 | 81 | When using Azure CLI, you may want to log in to Azure and set default subscription. This is a one-time operation: 82 | ```sh 83 | SUB= 84 | 85 | az login 86 | 87 | az account set --subscription ${SUB} 88 | ``` 89 | 90 | Create a resource group and deploy the VM: 91 | ```sh 92 | RGROUP= 93 | LOC= 94 | 95 | az group create -n ${RGROUP} -l ${LOC} 96 | 97 | az deployment group create \ 98 | --resource-group ${RGROUP} \ 99 | --name MyDeployment \ 100 | --template-file ./_output/azuredeploy.json \ 101 | --parameters @./_output/azuredeploy.parameters.json 102 | ``` 103 | -------------------------------------------------------------------------------- /parts/vars.t: -------------------------------------------------------------------------------- 1 | "imageList": { 2 | "Windows Server 2022 Gen 2": { 3 | "publisher": "microsoftwindowsserver", 4 | "offer": "windows-cvm", 5 | "sku": "2022-datacenter-cvm", 6 | "version": "latest" 7 | }, 8 | "Windows Server 2019 Gen 2": { 9 | "publisher": "microsoftwindowsserver", 10 | "offer": "windows-cvm", 11 | "sku": "2019-datacenter-cvm", 12 | "version": "latest" 13 | }, 14 | "Ubuntu 20.04 LTS Gen 2": { 15 | "publisher": "Canonical", 16 | "offer": "0001-com-ubuntu-confidential-vm-experimental", 17 | "sku": "20_04-gen2", 18 | "version": "20.04.20210309" 19 | }, 20 | "Ubuntu 18.04 LTS Gen 2": { 21 | "publisher": "Canonical", 22 | "offer": "0001-com-ubuntu-confidential-vm-experimental", 23 | "sku": "18_04-gen2", 24 | "version": "18.04.20210309" 25 | } 26 | }, 27 | "imageReference": "[variables('imageList')[parameters('osImageName')]]", 28 | "networkInterfaceName": "[concat(parameters('vmName'), '-nic')]", 29 | "publicIPAddressName": "[concat(parameters('vmName'), '-ip')]", 30 | "networkSecurityGroupName": "[concat(parameters('vmName'), '-nsg')]", 31 | "networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]", 32 | "virtualNetworkName": "[concat(parameters('vmName'), '-vnet')]", 33 | "virtualNetworkId": "[resourceId(resourceGroup().name, 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", 34 | "subnetRef": "[concat(variables('virtualNetworkId'), '/subnets/', variables('subnetName'))]", 35 | "subnetName": "[concat(parameters('vmName'), 'Subnet')]", 36 | "vnetSubnetId": "[resourceId(parameters('vnetResourceGroupName'), 'Microsoft.Network/virtualNetworks/subnets/', variables('virtualNetworkName'), parameters('subnetName'))]", 37 | "isWindows": "[contains(parameters('osImageName'), 'Windows')]", 38 | "linuxConfiguration": { 39 | "disablePasswordAuthentication": "true", 40 | "ssh": { 41 | "publicKeys": [ 42 | { 43 | "keyData": "[parameters('adminPasswordOrKey')]", 44 | "path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]" 45 | } 46 | ] 47 | } 48 | }, 49 | "windowsConfiguration": { 50 | "enableAutomaticUpdates": "true", 51 | "provisionVmAgent": "true" 52 | }, 53 | {{if HasTipNodeSession}} 54 | "availabilitySetName": "[concat(parameters('vmName'), '-availSet')]", 55 | {{end}} 56 | "isMemoryUnencrypted": "[equals(parameters('securityType'), 'Unencrypted')]", 57 | "vmStorageProfileManagedDisk": { 58 | "storageAccountType": "[parameters('osDiskType')]" 59 | }, 60 | "vmStorageProfileManagedDiskEncrypted": { 61 | "storageAccountType": "[parameters('osDiskType')]", 62 | "securityProfile": { 63 | "securityEncryptionType" : "[parameters('securityType')]" 64 | } 65 | }, 66 | "diagnosticsStorageAction": "[if(equals(parameters('bootDiagnostics'), 'false'), 'nop', parameters('diagnosticsStorageAccountNewOrExisting'))]", 67 | "vmSecurityProfile": { 68 | "uefiSettings" : { 69 | "secureBootEnabled": "[parameters('secureBootEnabled')]", 70 | "vTpmEnabled": "true" 71 | }, 72 | "securityType" : "ConfidentialVM" 73 | } 74 | -------------------------------------------------------------------------------- /pkg/api/tvm.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // tvmConfigurator implements VMConfigurator interface 10 | type tvmConfigurator struct { 11 | osName OSName 12 | } 13 | 14 | var tvmOSImageMap map[OSName]*OSImage 15 | 16 | func init() { 17 | tvmOSImageMap = map[OSName]*OSImage{ 18 | Ubuntu1804: &OSImage{ 19 | Publisher: "Canonical", 20 | Offer: "0003-com-ubuntu-server-trusted-vm", 21 | SKU: "18_04-gen2", 22 | Version: "18.04.202004290", 23 | }, 24 | Windows10: &OSImage{ 25 | Publisher: "MicrosoftWindowsServer", 26 | Offer: "windowsserver-gen2preview-preview", 27 | SKU: "windows10-tvm", 28 | Version: "18363.592.2001092016", 29 | }, 30 | WindowsServer2016: &OSImage{ 31 | Publisher: "MicrosoftWindowsServer", 32 | Offer: "windowsserver-gen2preview-preview", 33 | SKU: "windowsserver2016-tvm", 34 | Version: "14393.3443.2001090113", 35 | }, 36 | WindowsServer2019: &OSImage{ 37 | Publisher: "MicrosoftWindowsServer", 38 | Offer: "windowsserver-gen2preview-preview", 39 | SKU: "windowsserver2019-tvm", 40 | Version: "17763.973.2001110547", 41 | }, 42 | Windows10SecuredCore: &OSImage{ 43 | Publisher: "MicrosoftWindowsServer", 44 | Offer: "windowsserver-gen2preview-preview", 45 | SKU: "windows10-tvm-sc", 46 | Version: "19041.329.2006042020", 47 | }, 48 | WindowsServer2016SecuredCore: &OSImage{ 49 | Publisher: "MicrosoftWindowsServer", 50 | Offer: "windowsserver-gen2preview-preview", 51 | SKU: "windowsserver2016-tvm-sc", 52 | Version: "14393.3750.2006031549", 53 | }, 54 | WindowsServer2019SecuredCore: &OSImage{ 55 | Publisher: "MicrosoftWindowsServer", 56 | Offer: "windowsserver-gen2preview-preview", 57 | SKU: "windowsserver2019-tvm-sc", 58 | Version: "19041.329.2006042019", 59 | }, 60 | } 61 | } 62 | 63 | // NewTVMConfigurator creates VMConfigurator for TVM 64 | func NewTVMConfigurator(osName OSName) (VMConfigurator, error) { 65 | if len(osName) > 0 { 66 | if _, ok := tvmOSImageMap[osName]; !ok { 67 | return nil, fmt.Errorf("OSName %q is not supported", osName) 68 | } 69 | } 70 | return &tvmConfigurator{osName: osName}, nil 71 | } 72 | 73 | func (h *tvmConfigurator) DefaultVMName() string { 74 | return "tvm" 75 | } 76 | 77 | func (h *tvmConfigurator) OSImage() *OSImage { 78 | if len(h.osName) == 0 { 79 | log.Fatal("OSName is not set") 80 | } 81 | return tvmOSImageMap[h.osName] 82 | } 83 | 84 | func (h *tvmConfigurator) OSImageName() string { 85 | if len(h.osName) == 0 { 86 | log.Fatal("OSImageName is not set") 87 | } 88 | return "" 89 | } 90 | 91 | // DefaultLinuxSecurityType returns default Linux OS security type 92 | func (h *tvmConfigurator) SecurityType() string { 93 | log.Info("SecurityType is not set") 94 | return "Unencrypted" 95 | } 96 | 97 | 98 | // DefaultOsDiskType returns default OS disk type 99 | func (h *tvmConfigurator) DefaultOsDiskType() string { 100 | return "Premium_LRS" 101 | } 102 | 103 | // AllowedOsDiskTypes returns supported OS disk types 104 | func (h *tvmConfigurator) AllowedOsDiskTypes() []string { 105 | return []string{ 106 | "Premium_LRS", 107 | "StandardSSD_LRS", 108 | } 109 | } 110 | 111 | // AllowedVMSizes returns supported VM sizes 112 | func (h *tvmConfigurator) AllowedVMSizes() []string { 113 | return []string{ 114 | "Standard_DC2s", 115 | "Standard_DC4s", 116 | "Standard_DC1s_v2", 117 | "Standard_DC2s_v2", 118 | "Standard_DC4s_v2", 119 | "Standard_DC8_v2", 120 | "Standard_D2s_v3", 121 | "Standard_D4s_v3", 122 | "Standard_D8s_v3", 123 | "Standard_D16s_v3", 124 | "Standard_D32s_v3", 125 | "Standard_D64s_v3", 126 | } 127 | } 128 | 129 | func (h *tvmConfigurator) DefaultVMSize() string { 130 | return "Standard_DC2s_v2" 131 | } 132 | -------------------------------------------------------------------------------- /pkg/engine/transform/apimodel_merger.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/Jeffail/gabs" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // APIModelValue represents a value in the APIModel JSON file 16 | type APIModelValue struct { 17 | stringValue string 18 | intValue int64 19 | arrayValue bool 20 | arrayIndex int 21 | arrayProperty string 22 | arrayName string 23 | } 24 | 25 | // MapValues converts an arraw of rwa ApiModel values (like ["masterProfile.count=4","linuxProfile.adminUsername=admin"]) to a map 26 | func MapValues(m map[string]APIModelValue, values []string) { 27 | if values == nil || len(values) == 0 { 28 | return 29 | } 30 | 31 | for _, value := range values { 32 | splittedValues := strings.Split(value, ",") 33 | if len(splittedValues) > 1 { 34 | MapValues(m, splittedValues) 35 | } else { 36 | keyValueSplitted := strings.Split(value, "=") 37 | key := keyValueSplitted[0] 38 | stringValue := keyValueSplitted[1] 39 | 40 | flagValue := APIModelValue{} 41 | 42 | if asInteger, err := strconv.ParseInt(stringValue, 10, 64); err == nil { 43 | flagValue.intValue = asInteger 44 | } else { 45 | flagValue.stringValue = stringValue 46 | } 47 | 48 | // use regex to find array[index].property pattern in the key 49 | re := regexp.MustCompile(`(.*?)\[(.*?)\]\.(.*?)$`) 50 | match := re.FindStringSubmatch(key) 51 | 52 | // it's an array 53 | if len(match) != 0 { 54 | i, err := strconv.ParseInt(match[2], 10, 32) 55 | if err != nil { 56 | log.Warnln(fmt.Sprintf("array index is not specified for property %s", key)) 57 | } else { 58 | arrayIndex := int(i) 59 | flagValue.arrayValue = true 60 | flagValue.arrayName = match[1] 61 | flagValue.arrayIndex = arrayIndex 62 | flagValue.arrayProperty = match[3] 63 | m[key] = flagValue 64 | } 65 | } else { 66 | m[key] = flagValue 67 | } 68 | } 69 | } 70 | } 71 | 72 | // MergeValuesWithAPIModel takes the path to an ApiModel JSON file, loads it and merges it with the values in the map to another temp file 73 | func MergeValuesWithAPIModel(apiModelPath string, m map[string]APIModelValue) (string, error) { 74 | // load the apiModel file from path 75 | fileContent, err := ioutil.ReadFile(apiModelPath) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | // parse the json from file content 81 | jsonObj, err := gabs.ParseJSON(fileContent) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | // update api model definition with each value in the map 87 | for key, flagValue := range m { 88 | // working on an array 89 | if flagValue.arrayValue { 90 | log.Infoln(fmt.Sprintf("--set flag array value detected. Name: %s, Index: %b, PropertyName: %s", flagValue.arrayName, flagValue.arrayIndex, flagValue.arrayProperty)) 91 | arrayValue := jsonObj.Path(fmt.Sprint("properties.", flagValue.arrayName)) 92 | if flagValue.stringValue != "" { 93 | arrayValue.Index(flagValue.arrayIndex).SetP(flagValue.stringValue, flagValue.arrayProperty) 94 | } else { 95 | arrayValue.Index(flagValue.arrayIndex).SetP(flagValue.intValue, flagValue.arrayProperty) 96 | } 97 | } else { 98 | if flagValue.stringValue != "" { 99 | jsonObj.SetP(flagValue.stringValue, fmt.Sprint("properties.", key)) 100 | } else { 101 | jsonObj.SetP(flagValue.intValue, fmt.Sprint("properties.", key)) 102 | } 103 | } 104 | } 105 | 106 | // generate a new file 107 | tmpFile, err := ioutil.TempFile("", "mergedApiModel") 108 | if err != nil { 109 | return "", err 110 | } 111 | 112 | tmpFileName := tmpFile.Name() 113 | err = ioutil.WriteFile(tmpFileName, []byte(jsonObj.String()), os.ModeAppend) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | return tmpFileName, nil 119 | } 120 | -------------------------------------------------------------------------------- /pkg/api/common/versions.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/blang/semver" 7 | ) 8 | 9 | // GetVersionsGt returns a list of versions greater than a semver string given a list of versions 10 | // inclusive=true means that we test for equality as well 11 | // preReleases=true means that we include pre-release versions in the list 12 | func GetVersionsGt(versions []string, version string, inclusive, preReleases bool) []string { 13 | // Try to get latest version matching the release 14 | var ret []string 15 | minVersion, _ := semver.Make(version) 16 | for _, v := range versions { 17 | sv, _ := semver.Make(v) 18 | if !preReleases && len(sv.Pre) != 0 { 19 | continue 20 | } 21 | if (inclusive && sv.GTE(minVersion)) || (!inclusive && sv.GT(minVersion)) { 22 | ret = append(ret, v) 23 | } 24 | } 25 | return ret 26 | } 27 | 28 | // GetVersionsLt returns a list of versions less than than a semver string given a list of versions 29 | // inclusive=true means that we test for equality as well 30 | // preReleases=true means that we include pre-release versions in the list 31 | func GetVersionsLt(versions []string, version string, inclusive, preReleases bool) []string { 32 | // Try to get latest version matching the release 33 | var ret []string 34 | minVersion, _ := semver.Make(version) 35 | for _, v := range versions { 36 | sv, _ := semver.Make(v) 37 | if !preReleases && len(sv.Pre) != 0 { 38 | continue 39 | } 40 | if (inclusive && sv.LTE(minVersion)) || (!inclusive && sv.LT(minVersion)) { 41 | ret = append(ret, v) 42 | } 43 | } 44 | return ret 45 | } 46 | 47 | // GetVersionsBetween returns a list of versions between a min and max 48 | // inclusive=true means that we test for equality on both bounds 49 | // preReleases=true means that we include pre-release versions in the list 50 | func GetVersionsBetween(versions []string, versionMin, versionMax string, inclusive, preReleases bool) []string { 51 | var ret []string 52 | if minV, _ := semver.Make(versionMin); len(minV.Pre) != 0 { 53 | preReleases = true 54 | } 55 | greaterThan := GetVersionsGt(versions, versionMin, inclusive, preReleases) 56 | lessThan := GetVersionsLt(versions, versionMax, inclusive, preReleases) 57 | for _, lv := range lessThan { 58 | for _, gv := range greaterThan { 59 | if lv == gv { 60 | ret = append(ret, lv) 61 | } 62 | } 63 | } 64 | return ret 65 | } 66 | 67 | // GetMaxVersion gets the highest semver version 68 | // preRelease=true means accept a pre-release version as a max value 69 | func GetMaxVersion(versions []string, preRelease bool) string { 70 | if len(versions) < 1 { 71 | return "" 72 | } 73 | highest, _ := semver.Make("0.0.0") 74 | highestPreRelease, _ := semver.Make("0.0.0-alpha.0") 75 | var preReleaseVersions []semver.Version 76 | for _, v := range versions { 77 | sv, _ := semver.Make(v) 78 | if len(sv.Pre) != 0 { 79 | preReleaseVersions = append(preReleaseVersions, sv) 80 | } else { 81 | if sv.Compare(highest) == 1 { 82 | highest = sv 83 | } 84 | } 85 | } 86 | if preRelease { 87 | for _, sv := range preReleaseVersions { 88 | if sv.Compare(highestPreRelease) == 1 { 89 | highestPreRelease = sv 90 | } 91 | } 92 | switch highestPreRelease.Compare(highest) { 93 | case 1: 94 | return highestPreRelease.String() 95 | default: 96 | return highest.String() 97 | } 98 | 99 | } 100 | return highest.String() 101 | } 102 | 103 | // GetLatestPatchVersion gets the most recent patch version from a list of semver versions given a major.minor string 104 | func GetLatestPatchVersion(majorMinor string, versionsList []string) (version string) { 105 | // Try to get latest version matching the release 106 | version = "" 107 | for _, ver := range versionsList { 108 | sv, err := semver.Make(ver) 109 | if err != nil { 110 | return 111 | } 112 | sr := fmt.Sprintf("%d.%d", sv.Major, sv.Minor) 113 | if sr == majorMinor { 114 | if version == "" { 115 | version = ver 116 | } else { 117 | current, _ := semver.Make(version) 118 | if sv.GT(current) { 119 | version = ver 120 | } 121 | } 122 | } 123 | } 124 | return version 125 | } 126 | -------------------------------------------------------------------------------- /pkg/engine/pki.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "errors" 11 | "math/big" 12 | "net" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // ValidityDuration specifies the duration an TLS certificate is valid 18 | ValidityDuration = time.Hour * 24 * 365 * 2 19 | // PkiKeySize is the size in bytes of the PKI key 20 | PkiKeySize = 4096 21 | ) 22 | 23 | // PkiKeyCertPair represents an PKI public and private cert pair 24 | type PkiKeyCertPair struct { 25 | CertificatePem string 26 | PrivateKeyPem string 27 | } 28 | 29 | func createCertificate(commonName string, caCertificate *x509.Certificate, caPrivateKey *rsa.PrivateKey, isEtcd bool, isServer bool, extraFQDNs []string, extraIPs []net.IP, organization []string) (*x509.Certificate, *rsa.PrivateKey, error) { 30 | var err error 31 | 32 | isCA := (caCertificate == nil) 33 | 34 | now := time.Now() 35 | 36 | template := x509.Certificate{ 37 | Subject: pkix.Name{CommonName: commonName}, 38 | NotBefore: now, 39 | NotAfter: now.Add(ValidityDuration), 40 | 41 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 42 | BasicConstraintsValid: true, 43 | } 44 | 45 | if organization != nil { 46 | template.Subject.Organization = organization 47 | } 48 | 49 | if isCA { 50 | template.KeyUsage |= x509.KeyUsageCertSign 51 | template.IsCA = isCA 52 | } else if isEtcd { 53 | if commonName == "etcdServer" { 54 | template.IPAddresses = extraIPs 55 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) 56 | } else if commonName == "etcdClient" { 57 | template.IPAddresses = extraIPs 58 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) 59 | } else { 60 | template.IPAddresses = extraIPs 61 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) 62 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) 63 | } 64 | } else if isServer { 65 | template.DNSNames = extraFQDNs 66 | template.IPAddresses = extraIPs 67 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) 68 | } else { 69 | template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) 70 | } 71 | 72 | snMax := new(big.Int).Lsh(big.NewInt(1), 128) 73 | template.SerialNumber, err = rand.Int(rand.Reader, snMax) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | 78 | privateKey, _ := rsa.GenerateKey(rand.Reader, PkiKeySize) 79 | 80 | var privateKeyToUse *rsa.PrivateKey 81 | var certificateToUse *x509.Certificate 82 | if !isCA { 83 | privateKeyToUse = caPrivateKey 84 | certificateToUse = caCertificate 85 | } else { 86 | privateKeyToUse = privateKey 87 | certificateToUse = &template 88 | } 89 | 90 | certDerBytes, err := x509.CreateCertificate(rand.Reader, &template, certificateToUse, &privateKey.PublicKey, privateKeyToUse) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | certificate, err := x509.ParseCertificate(certDerBytes) 96 | if err != nil { 97 | return nil, nil, err 98 | } 99 | 100 | return certificate, privateKey, nil 101 | } 102 | 103 | func certificateToPem(derBytes []byte) []byte { 104 | pemBlock := &pem.Block{ 105 | Type: "CERTIFICATE", 106 | Bytes: derBytes, 107 | } 108 | pemBuffer := bytes.Buffer{} 109 | pem.Encode(&pemBuffer, pemBlock) 110 | 111 | return pemBuffer.Bytes() 112 | } 113 | 114 | func privateKeyToPem(privateKey *rsa.PrivateKey) []byte { 115 | pemBlock := &pem.Block{ 116 | Type: "RSA PRIVATE KEY", 117 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 118 | } 119 | pemBuffer := bytes.Buffer{} 120 | pem.Encode(&pemBuffer, pemBlock) 121 | 122 | return pemBuffer.Bytes() 123 | } 124 | 125 | func pemToCertificate(raw string) (*x509.Certificate, error) { 126 | cpb, _ := pem.Decode([]byte(raw)) 127 | if cpb == nil { 128 | return nil, errors.New("The raw pem is not a valid PEM formatted block") 129 | } 130 | return x509.ParseCertificate(cpb.Bytes) 131 | } 132 | 133 | func pemToKey(raw string) (*rsa.PrivateKey, error) { 134 | kpb, _ := pem.Decode([]byte(raw)) 135 | if kpb == nil { 136 | return nil, errors.New("The raw pem is not a valid PEM formatted block") 137 | } 138 | return x509.ParsePKCS1PrivateKey(kpb.Bytes) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/engine/ssh_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 8 | ) 9 | 10 | func TestCreateSSH(t *testing.T) { 11 | rg := rand.New(rand.NewSource(42)) 12 | 13 | expectedPublicKeyString := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyx5MHXjJvJAx5DJ9FZNIDa/QTWorSF+Ra21Tz49DQWfdSESnCGFFVBh/MQUFGv5kCenbmqEjsWF177kFOdv1vOTz4sKRlHg7u3I9uCyyZQrWx4X4RdNk7eX+isQVjFXYw2W1rRDUrnK/82qVTv1f0gu1DV4Z7GoIa2jfJ0zBUY3IW0VN9jYaPVuwv4t5y2GwSZF+HBRuOfLfiUgt4+qVFOz4KwRaEBsVfWxlidlT3K3/+ztWpFOmaKIOjQreEWV10ZSo3f9g6j/HdMPtwYvRCtYStbFCRmcbPr9nuR84SAX/4f95KvBAKLnXwb5Bt71D2vAlZSW1Ylv2VbcaZ73+43EpyphYCSg3kOCdwsqE/EU+Swued82SguLALD3mNKbxHGJppFjz3GMyPpJuSH5EE1OANyPxABCwCYycKiNWbOPi3l6o4tMrASYRXi8l3l9JCvioUJ3bXXH6cDpcP4P6QgsuxhwVkUiECU+dbjJXK4gAUVuWKkMOdY7ITh82oU3wOWXbk8K3bdIUp2ylcHeAd2pekGMuaEKGbrXGRiBitCEjl67Bj5opQflgSmI63g8Sa3mKOPGRYMI5MXHMVj4Rns5JFHoENuImrlvrbLv3izAwO61vgN7iK26BwzO7jz92fNOHGviejNWYJyi4vZlq07153NZXP8D2xYTebh9hwHQ==\n" 14 | 15 | expectedPrivateKeyString := `-----BEGIN RSA PRIVATE KEY----- 16 | MIIJKgIBAAKCAgEAsseTB14ybyQMeQyfRWTSA2v0E1qK0hfkWttU8+PQ0Fn3UhEp 17 | whhRVQYfzEFBRr+ZAnp25qhI7Fhde+5BTnb9bzk8+LCkZR4O7tyPbgssmUK1seF+ 18 | EXTZO3l/orEFYxV2MNlta0Q1K5yv/NqlU79X9ILtQ1eGexqCGto3ydMwVGNyFtFT 19 | fY2Gj1bsL+LecthsEmRfhwUbjny34lILePqlRTs+CsEWhAbFX1sZYnZU9yt//s7V 20 | qRTpmiiDo0K3hFlddGUqN3/YOo/x3TD7cGL0QrWErWxQkZnGz6/Z7kfOEgF/+H/e 21 | SrwQCi518G+Qbe9Q9rwJWUltWJb9lW3Gme9/uNxKcqYWAkoN5DgncLKhPxFPksLn 22 | nfNkoLiwCw95jSm8RxiaaRY89xjMj6Sbkh+RBNTgDcj8QAQsAmMnCojVmzj4t5eq 23 | OLTKwEmEV4vJd5fSQr4qFCd211x+nA6XD+D+kILLsYcFZFIhAlPnW4yVyuIAFFbl 24 | ipDDnWOyE4fNqFN8Dll25PCt23SFKdspXB3gHdqXpBjLmhChm61xkYgYrQhI5euw 25 | Y+aKUH5YEpiOt4PEmt5ijjxkWDCOTFxzFY+EZ7OSRR6BDbiJq5b62y794swMDutb 26 | 4De4itugcMzu48/dnzThxr4nozVmCcouL2ZatO9edzWVz/A9sWE3m4fYcB0CAwEA 27 | AQKCAgEArQmNvWvm1LvHdsJIxhm3S6iJLNJN2ttVIrt3ljfCPGdXgg8qo7p1vh2X 28 | WVMvoxJ/Pm7Z9pabPmao1PLeMtvooGZ+JRaTh2t4eKjyCki2egCfa/Qc2TiHqZEH 29 | gKhl1mlHZDCOP2xdKkEV9V6K9mwU7YxrqOpmN3CIzQS5SpcmCAfYvU0Nyk/ZFZPE 30 | NvUW6YGf2I1eCIlhCqCcOmm+wPGYVVHp0u7gpBkJoCnEgBCYXEO2NyJqmqSrFZJx 31 | FuvURD1avvXLzrvmxYfdSYHHXBfq40ZdjJ1xvftg+lPyUzcctUDOY+8fcKZlv/UI 32 | IhdZa45ehvGo+sqfE0fRWXhO6V9t9hdHwOq6ZEF2TtaA9qwPpZxiN5BN7G6Vi6Bm 33 | u3HhSCHyEIdySi9/hX3fhDrhPN08NULLhpiKuSiFQesmUxFxWAprMpEyCdx0wva7 34 | 5tZTQQfmVHCoWyVXWNMGTGBA/h8SWquoQWWhpG7UWCt0A0e0kcbegZTQPddxgITe 35 | uqf6GadbajAr6Qwicf5yNH7bVPiD8dGWU07W3t4C0JyLGNLN34aT0OpleSck4dGp 36 | V2UYylQNkf/EmxTY/CCPtNVVKng3CJ+jZvS4MOKvTi+vvsccd8x6BEo9xKetJhAA 37 | SQeNDMu9tEPlZNHC972YNLb+LPm+feqgM2W/qcONtNhPw1INW+ECggEBAOmPO9jz 38 | q6Gm8nNoALteuAD58pJ/suJTfhXbkGBOCG+hazlmk3rGzf9G/cK2jbS3ePoHw7b9 39 | oJcpoF2L1nUCdwxTJMUS+iyfVRQ4L8lRDC95x3vdBcdgFZUQgEx1L6hKuK5BpZOY 40 | fyvIEmwpW7OpCOEqXeMOq3agR4//uptIyNCzyIPJz43H0dh6m4l+fYy53AOvDAeW 41 | Xk0wERP6bolngkVnz1XbE43UNZqTFkGMF4gjJCbZ+UguOltsZXSPLA+ruRy3oYGn 42 | LVo1ntAf8Ih94F43Y8Doe+VX3y2UJUqQa/ZFG2nu6KeuDWhwRS/XZQSkxrJ0bO2w 43 | 6eOCOEqggO7Qz7sCggEBAMP08Q1nPfmwdawEYWqopKeAMh00oMoX14u8UDmYejiH 44 | uBegwzqgmOLfajFMJDnNXTyzxIRIndzrvXzvtFpSHkh29sOXXG9xlGyLWZGcxtzW 45 | ivyTMw/pTg3yjN0qsleRB/o89VOYP2OG+1XjEcie6LNxXUN/wG5gUx8Wumb2c1hW 46 | XBDM6cRbiSuJuINjscUgiHXKQddfu1cVRaNUgP1PGniKydCqdI2rUUQhziTmmj+o 47 | q+dSv6nGRaK3uNhJrhpMlljxy1Mcr9zLP5FM1GjaF+VQ3zHNxDDbXl13rQPpDocw 48 | vu9tAS/J1+vTgKzcHjKnudUWmoNahT3f4/86fc6XJgcCggEBAMK4ry3Goa5JUNPU 49 | vt94LbJqsMlg+9PjxjgU8T7JcBEZpBqcIZL4EqClIEXpCyXC3XKfbJWwyOWeR9wW 50 | DPtKzdQRsZM4qijvwe/0lCqkjqM6RY1IDVxXCEdaFY0pGk2V1nk5tADk4AmxaWKR 51 | 7KlR4VxQhSwbe+qP4Hn2vC5gtUQCz8bIR2muUY7JUcmFEslz3zGXDFF7FS4HSAW/ 52 | Ac8+5AZXcS3kU14osXQo8yI82RWgLrDRhBqgp/i227Mc9qAuDEwb8OP2bEJMeBaO 53 | umwhfiEuztTzPvBLnX8Thy+uTsRog12DWKcL3pPXHmevjcIcWqhHltVobOdIFwRo 54 | 4nW406cCggEBALmwZ6hy2Ai/DZL3B7VBn93WHicM0v0OwMN6rG8XrWHaQjmprrbk 55 | rlv2qDOU2pMnpx25oBRWl7lcbtBweXBJdsbmbIoF6aL1d1ewaS0R6mQkrcoQVwfR 56 | 5pRS7uc56YwPNAcOMs+HazIOHCdUKGr7IrnASEeJTLmLb9j6+aJOEhl4pH+LHk5j 57 | C0YFmKJxG2kYnhc4lVHZNrabwsS2dBEWH5hwtDOXAyGoYTb17dmL6ElAtb1b7aGc 58 | 8Cn0fSYAFAp53tLkNe9JNOE+fLtcmb/OQ2ybSRVxzmMZzX82w+37sDetmpFZsxEs 59 | 7P5dCwdDAx6vT+q8I6krYy2x9uTJ8aOOGYsCggEAAW9qf3UNuY0IB9kmHF3Oo1gN 60 | s82h0OLpjJkW+5YYC0vYQit4AYNjXw+T+Z6WKOHOG3LIuQVC6Qj4c1+oN6sJi7re 61 | Ey6Zq7/uWmYUpi9C8CbX1clJwany0V2PjGKL94gCIl7vaXS/4ouzzfl8qbF7FjQ4 62 | Qq/HPWSIC9Z8rKtUDDHeZYaLqvdhqbas/drqCXmeLeYM6Om4lQJdP+zip3Ctulp1 63 | EPDesL0rH+3s1CKpgkhYdbJ675GFoGoq+X21QaqsdvoXmmuJF9qq9Tq+JaWloUNq 64 | 2FWXLhSX02saIdbIheS1fv/LqekXZd8eFXUj7VZ15tPG3SJqORS0pMtxSAJvLw== 65 | -----END RSA PRIVATE KEY----- 66 | ` 67 | 68 | privateKey, publicKey, err := helpers.CreateSSH(rg) 69 | if err != nil { 70 | t.Fatalf("failed to generate SSH: %s", err) 71 | } 72 | privateKeyString := string(privateKeyToPem(privateKey)) 73 | 74 | if privateKeyString != expectedPrivateKeyString { 75 | t.Fatalf("Private Key did not match expected format/value") 76 | } 77 | 78 | if publicKey != expectedPublicKeyString { 79 | t.Fatalf("Public Key did not match expected format/value") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/strictjson_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type SubTestProfile struct { 10 | SP1 bool `json:"sp1"` 11 | SP2 bool `json:"sp2"` 12 | SP3 []SubTestProfile `json:"sp3,omitempty"` 13 | } 14 | 15 | type TestProfile struct { 16 | Field1 int `json:"f1"` 17 | Field2 SubTestProfile `json:"f2,omitempty"` 18 | Field3 []SubTestProfile `json:"f3,omitempty"` 19 | Field4 *SubTestProfile `json:"f4"` 20 | Field5 []*SubTestProfile `json:"f5,omitempty"` 21 | } 22 | 23 | func TestIfAllJSONKeysAreExpectedThenCheckPasses(t *testing.T) { 24 | json := ` 25 | { 26 | "f1": 1, 27 | "f2": { 28 | "sp1": true, 29 | "sp2": false 30 | } 31 | } 32 | ` 33 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 34 | if e != nil { 35 | t.Errorf("All JSON keys were expected but the check still failed: %v", e) 36 | } 37 | } 38 | 39 | func TestIsCaseInsensitive(t *testing.T) { 40 | json := ` 41 | { 42 | "F1": 1, 43 | "f2": { 44 | "sP1": true, 45 | "Sp2": false 46 | } 47 | } 48 | ` 49 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 50 | if e != nil { 51 | t.Errorf("All JSON keys were expected (allowing for case) but the check still failed: %v", e) 52 | } 53 | } 54 | 55 | func TestCheckFailsOnUnexpectedJSONKeyAtTopLevel(t *testing.T) { 56 | json := ` 57 | { 58 | "f2": { 59 | "sp1": true, 60 | "sp2": false 61 | }, 62 | "fx": "uh-oh" 63 | } 64 | ` 65 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 66 | if e == nil { 67 | t.Fatal("Unexpected JSON key was not detected") 68 | } 69 | if !strings.Contains(e.Error(), "fx") { 70 | t.Errorf("Error message did not name unexpected JSON key 'fx': was %v", e) 71 | } 72 | } 73 | 74 | func TestCheckFailsOnUnexpectedJSONKeyAtSubLevel(t *testing.T) { 75 | json := ` 76 | { 77 | "f2": { 78 | "sp1": true, 79 | "spx": false 80 | } 81 | } 82 | ` 83 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 84 | if e == nil { 85 | t.Fatal("Unexpected JSON key was not detected") 86 | } 87 | if !strings.Contains(e.Error(), "spx") { 88 | t.Errorf("Error message did not name unexpected JSON key 'spx': was %v", e) // TODO: f2.spx would be better 89 | } 90 | } 91 | 92 | func TestCheckFailsOnUnexpectedJSONKeyInArray(t *testing.T) { 93 | json := ` 94 | { 95 | "f2": { 96 | "sp1": true, 97 | "sp3": [ 98 | { 99 | "sp1": false, 100 | "sp2": true 101 | }, 102 | { 103 | "sp2": false, 104 | "spz": "unexpected" 105 | } 106 | ] 107 | }, 108 | "f3": [] 109 | } 110 | ` 111 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 112 | if e == nil { 113 | t.Fatal("Unexpected JSON key was not detected") 114 | } 115 | if !strings.Contains(e.Error(), "spz") { 116 | t.Errorf("Error message did not name unexpected JSON key 'spz': was %v", e) // TODO: f2[1].spz might be better 117 | } 118 | } 119 | 120 | func TestCheckFailsOnUnexpectedJSONKeyInArrayAtSubLevel(t *testing.T) { 121 | json := ` 122 | { 123 | "f2": { 124 | "sp1": true 125 | }, 126 | "f3": [ 127 | { 128 | "sp1": true, 129 | "spy": "unexpected" 130 | }, 131 | { 132 | "sp2": false, 133 | "spz": "unexpected" 134 | } 135 | ] 136 | } 137 | ` 138 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 139 | if e == nil { 140 | t.Fatal("Unexpected JSON key was not detected") 141 | } 142 | if !strings.Contains(e.Error(), "spy") { 143 | t.Errorf("Error message did not name unexpected JSON key 'spy': was %v", e) // TODO: f3[0].spy might be better 144 | } 145 | } 146 | 147 | func TestCheckFailsOnUnexpectedJSONKeyAtSubLevelViaPointer(t *testing.T) { 148 | json := ` 149 | { 150 | "f4": { 151 | "sp1": true, 152 | "spx": false 153 | } 154 | } 155 | ` 156 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 157 | if e == nil { 158 | t.Fatal("Unexpected JSON key was not detected") 159 | } 160 | if !strings.Contains(e.Error(), "spx") { 161 | t.Errorf("Error message did not name unexpected JSON key 'spx': was %v", e) // TODO: f4.spx would be better 162 | } 163 | } 164 | 165 | func TestCheckFailsOnUnexpectedJSONKeyInArrayAtSubLevelViaPointer(t *testing.T) { 166 | json := ` 167 | { 168 | "f2": { 169 | "sp1": true 170 | }, 171 | "f5": [ 172 | { 173 | "sp1": true, 174 | "spy": "unexpected" 175 | }, 176 | { 177 | "sp2": false, 178 | "spz": "unexpected" 179 | } 180 | ] 181 | } 182 | ` 183 | e := checkJSONKeys([]byte(json), reflect.TypeOf(TestProfile{})) 184 | if e == nil { 185 | t.Fatal("Unexpected JSON key was not detected") 186 | } 187 | if !strings.Contains(e.Error(), "spy") { 188 | t.Errorf("Error message did not name unexpected JSON key 'spy': was %v", e) // TODO: f3[0].spy might be better 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /pkg/api/types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // VMCategory represents VM category 4 | type VMCategory string 5 | 6 | // APIModel complies with the ARM model of 7 | // resource definition in a JSON template. 8 | type APIModel struct { 9 | VMCategory VMCategory `json:"vm_category"` 10 | Properties *Properties `json:"properties,omitempty"` 11 | 12 | VMConfigurator VMConfigurator 13 | } 14 | 15 | // OSType represents OS type 16 | type OSType string 17 | 18 | // OSName represents pre-set OS name 19 | type OSName string 20 | 21 | // OSImageName represents pre-set OS image name 22 | type OSImageName string 23 | 24 | // OSImageName represents pre-set OS image name 25 | type SecurityType string 26 | 27 | // SecurityProfile represents VM security profile 28 | type SecurityProfile struct { 29 | SecureBoot string `json:"secure_boot_enabled,omitempty"` 30 | VTPM string `json:"vtpm_enabled,omitempty"` 31 | } 32 | 33 | // VMProfile represents the definition of a VM 34 | type VMProfile struct { 35 | Name string `json:"name"` 36 | OSImageName OSImageName `json:"os_image_name,omitempty"` 37 | OSName OSName `json:"os_name,omitempty"` 38 | OSDiskType string `json:"os_disk_type"` 39 | OSImage *OSImage `json:"os_image,omitempty"` 40 | DiskSizesGB []int `json:"disk_sizes_gb,omitempty"` 41 | VMSize string `json:"vm_size"` 42 | Ports []int `json:"ports,omitempty" validate:"dive,min=1,max=65535"` 43 | HasDNSName bool `json:"has_dns_name"` 44 | SecurityProfile *SecurityProfile `json:"security_profile,omitempty"` 45 | SecurityType SecurityType `json:"security_type,omitempty"` 46 | TipNodeSessionID string `json:"tip_node_session_id,omitempty"` 47 | ClusterName string `json:"cluster_name,omitempty"` 48 | } 49 | 50 | // Properties represents the ACS cluster definition 51 | type Properties struct { 52 | VnetProfile *VnetProfile `json:"vnet_rofile"` 53 | VMProfile *VMProfile `json:"vm_profile"` 54 | LinuxProfile *LinuxProfile `json:"linux_profile,omitempty"` 55 | WindowsProfile *WindowsProfile `json:"windows_profile,omitempty"` 56 | DiagnosticsProfile *DiagnosticsProfile `json:"diagnostics_profile,omitempty"` 57 | } 58 | 59 | // OSImage represents OS Image from Azure Image Gallery 60 | type OSImage struct { 61 | URL string `json:"url,omitempty"` 62 | Publisher string `json:"publisher"` 63 | Offer string `json:"offer"` 64 | SKU string `json:"sku"` 65 | Version string `json:"version,omitempty"` 66 | } 67 | 68 | // LinuxProfile represents the linux parameters passed to the cluster 69 | type LinuxProfile struct { 70 | AuthenticationType string `json:"authentication_type" validate:"required"` 71 | AdminUsername string `json:"admin_username" validate:"required"` 72 | AdminPasswordOrKey string `json:"admin_password_or_key"` 73 | SSHPubKeys []*PublicKey `json:"ssh_public_keys"` 74 | } 75 | 76 | // WindowsProfile represents the windows parameters passed to the cluster 77 | type WindowsProfile struct { 78 | AdminUsername string `json:"admin_username" validate:"required"` 79 | AdminPasswordOrKey string `json:"admin_password_or_key" validate:"required"` 80 | } 81 | 82 | // VnetProfile represents the definition of a vnet 83 | type VnetProfile struct { 84 | VnetResourceGroup string `json:"vnetResourceGroup,omitempty"` 85 | VirtualNetworkName string `json:"virtualNetworkName,omitempty"` 86 | VnetAddress string `json:"vnetAddress,omitempty"` 87 | SubnetName string `json:"subnetName,omitempty"` 88 | SubnetAddress string `json:"subnetAddress,omitempty"` 89 | } 90 | 91 | // DiagnosticsProfile contains settings to on/off boot diagnostics collection 92 | // in RD Host 93 | type DiagnosticsProfile struct { 94 | Enabled bool `json:"true"` 95 | StorageAccountName string `json:"storageAccountName"` 96 | IsNewStorageAccount bool `json:"isNewStorageAccount"` 97 | } 98 | 99 | // PublicKey contains puvlic SSH key 100 | type PublicKey struct { 101 | KeyData string `json:"key_data"` 102 | } 103 | 104 | // IsCustomVNET returns true if the customer brought their own VNET 105 | func (p *VnetProfile) IsCustomVNET() bool { 106 | return len(p.VnetResourceGroup) > 0 && len(p.VirtualNetworkName) > 0 && len(p.SubnetName) > 0 107 | } 108 | 109 | // HasAzureGalleryImage returns true if Azure Image Gallery is used 110 | func (h *VMProfile) HasAzureGalleryImage() bool { 111 | return h.OSImage != nil && len(h.OSImage.Publisher) > 0 && len(h.OSImage.Offer) > 0 && len(h.OSImage.SKU) > 0 112 | } 113 | 114 | // HasCustomOsImage returns true if there is a custom OS image url specified 115 | func (h *VMProfile) HasCustomOsImage() bool { 116 | return h.OSImage != nil && len(h.OSImage.URL) > 0 117 | } 118 | 119 | // HasDisks returns true if the customer specified disks 120 | func (h *VMProfile) HasDisks() bool { 121 | return len(h.DiskSizesGB) > 0 122 | } 123 | -------------------------------------------------------------------------------- /parts/params.t: -------------------------------------------------------------------------------- 1 | "vmName": { 2 | "type": "string", 3 | "metadata": { 4 | "description": "Name of the VM." 5 | } 6 | }, 7 | "vmSize": { 8 | "type": "string", 9 | {{GetVMSizes}} 10 | "metadata": { 11 | "description": "Size of the VM." 12 | } 13 | }, 14 | "authenticationType": { 15 | "type": "string", 16 | "defaultValue": "password", 17 | "allowedValues": [ 18 | "password", 19 | "sshPublicKey" 20 | ], 21 | "metadata": { 22 | "description": "Type of authentication to use on virtual machine (password for Windows, ssh public key for Linux)." 23 | } 24 | }, 25 | "adminUsername": { 26 | "type": "string", 27 | "defaultValue": "azureuser", 28 | "metadata": { 29 | "description": "Username for the Virtual Machine." 30 | } 31 | }, 32 | "adminPasswordOrKey": { 33 | "type": "securestring", 34 | "defaultValue": "", 35 | "metadata": { 36 | "description": "Password or ssh public key for the Virtual Machine." 37 | } 38 | }, 39 | "osImageName": { 40 | "type": "string", 41 | "defaultValue": "Windows Server 2022 Gen 2", 42 | "allowedValues": [ 43 | "Windows Server 2022 Gen 2", 44 | "Windows Server 2019 Gen 2", 45 | "Ubuntu 20.04 LTS Gen 2", 46 | "Ubuntu 18.04 LTS Gen 2" 47 | ], 48 | "metadata": { 49 | "description": "OS image name for the Virtual Machine" 50 | } 51 | }, 52 | "osDiskType": { 53 | "type": "string", 54 | "defaultValue": "Standard_LRS", 55 | "allowedValues": [ 56 | "Premium_LRS", 57 | "StandardSSD_LRS", 58 | "Standard_LRS" 59 | ], 60 | "metadata": { 61 | "description": "Type of managed disk to create." 62 | } 63 | }, 64 | "securityType": { 65 | "type": "string", 66 | "defaultValue": "VMGuestStateOnly", 67 | "allowedValues": [ 68 | "Unencrypted", 69 | "VMGuestStateOnly", 70 | "DiskWithVMGuestState" 71 | ], 72 | "metadata": { 73 | "description": "VM security type." 74 | } 75 | }, 76 | "vnetNewOrExisting": { 77 | "type": "string", 78 | "defaultValue": "new", 79 | "allowedValues": [ 80 | "new", 81 | "existing" 82 | ], 83 | "metadata": { 84 | "description": "Determines whether or not a new virtual network should be provisioned" 85 | } 86 | }, 87 | "vnetResourceGroupName": { 88 | "type": "string", 89 | "defaultValue": "[resourceGroup().name]", 90 | "metadata": { 91 | "description": "Name of the resource group for the existing virtual network." 92 | } 93 | }, 94 | "vnetAddress": { 95 | "type": "string", 96 | "defaultValue": "{{.VnetProfile.VnetAddress}}", 97 | "metadata": { 98 | "description": "VNET address space" 99 | } 100 | }, 101 | "addressPrefix": { 102 | "type": "string", 103 | "defaultValue": "10.1.16.0/24", 104 | "metadata": { 105 | "description": "VNET address space" 106 | } 107 | }, 108 | "subnetName": { 109 | "type": "string", 110 | "defaultValue": "[concat(resourceGroup().name, '-subnet')]", 111 | "metadata": { 112 | "description": "Name of the subnet." 113 | } 114 | }, 115 | "subnetAddress": { 116 | "type": "string", 117 | "defaultValue": "{{.VnetProfile.SubnetAddress}}", 118 | "metadata": { 119 | "description": "Sets the subnet of the VM." 120 | } 121 | }, 122 | "subnetPrefix": { 123 | "type": "string", 124 | "defaultValue": "10.1.16.0/24", 125 | "metadata": { 126 | "description": "Sets the subnet of the VM." 127 | } 128 | }, 129 | "tipNodeSessionId": { 130 | "type": "string", 131 | "defaultValue": "", 132 | "metadata": { 133 | "description": "TIP Node session ID" 134 | } 135 | }, 136 | "clusterName": { 137 | "type": "string", 138 | "defaultValue": "", 139 | "metadata": { 140 | "description": "Cluster" 141 | } 142 | }, 143 | {{if HasSecurityProfile}} 144 | "secureBootEnabled": { 145 | "type": "string", 146 | "defaultValue": "true", 147 | "allowedValues": [ 148 | "true", 149 | "false", 150 | "none" 151 | ], 152 | "metadata": { 153 | "description": "Secure Boot setting of the VM." 154 | } 155 | }, 156 | "vTPMEnabled": { 157 | "type": "string", 158 | "defaultValue": "true", 159 | "allowedValues": [ 160 | "true", 161 | "false" 162 | ], 163 | "metadata": { 164 | "description": "vTPM setting of the VM." 165 | } 166 | }, 167 | {{end}} 168 | "bootDiagnostics": { 169 | "type": "string", 170 | "defaultValue": "false", 171 | "allowedValues": [ 172 | "true", 173 | "false" 174 | ], 175 | "metadata": { 176 | "description": "Boot diagnostics setting of the VM." 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /parts/resources.t: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "type": "Microsoft.Network/publicIPAddresses", 4 | "apiVersion": "2019-02-01", 5 | "name": "[variables('publicIPAddressName')]", 6 | "location": "[resourceGroup().location]", 7 | "properties": { 8 | {{if HasDNSName .}} 9 | "dnsSettings": { 10 | "domainNameLabel": "[parameters('vmName')]" 11 | }, 12 | {{end}} 13 | "publicIPAllocationMethod": "Dynamic" 14 | } 15 | }, 16 | { 17 | "type": "Microsoft.Network/networkSecurityGroups", 18 | "apiVersion": "2019-02-01", 19 | "name": "[variables('networkSecurityGroupName')]", 20 | "location": "[resourceGroup().location]", 21 | "properties": { 22 | "securityRules": [ 23 | {{GetSecurityRules .VMProfile.Ports}} 24 | ] 25 | } 26 | }, 27 | { 28 | "condition": "[equals(parameters('vnetNewOrExisting'), 'new')]", 29 | "type": "Microsoft.Network/virtualNetworks", 30 | "apiVersion": "2019-09-01", 31 | "name": "[variables('virtualNetworkName')]", 32 | "location": "[resourceGroup().location]", 33 | "dependsOn": [ 34 | "[variables('networkSecurityGroupId')]" 35 | ], 36 | "properties": { 37 | "addressSpace": { 38 | "addressPrefixes": [ 39 | "[parameters('addressPrefix')]" 40 | ] 41 | }, 42 | "subnets": [ 43 | { 44 | "name": "[variables('subnetName')]", 45 | "properties": { 46 | "addressPrefix": "[parameters('subnetPrefix')]", 47 | "networkSecurityGroup": { 48 | "id": "[variables('networkSecurityGroupId')]" 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | }, 55 | { 56 | "type": "Microsoft.Network/networkInterfaces", 57 | "apiVersion": "2019-07-01", 58 | "name": "[variables('networkInterfaceName')]", 59 | "location": "[resourceGroup().location]", 60 | "dependsOn": [ 61 | "[variables('networkSecurityGroupId')]", 62 | "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]", 63 | "[concat('Microsoft.Network/publicIpAddresses/', variables('publicIpAddressName'))]" 64 | ], 65 | "properties": { 66 | "ipConfigurations": [ 67 | { 68 | "name": "ipConfigNode", 69 | "properties": { 70 | "privateIPAllocationMethod": "Dynamic", 71 | "subnet": { 72 | "id": "[variables('subnetRef')]" 73 | }, 74 | "publicIpAddress": { 75 | "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" 76 | } 77 | } 78 | } 79 | ], 80 | "networkSecurityGroup": { 81 | "id": "[variables('networkSecurityGroupId')]" 82 | } 83 | } 84 | }, 85 | {{if HasTipNodeSession}} 86 | { 87 | "type": "Microsoft.Compute/availabilitySets", 88 | "apiVersion": "2020-06-01", 89 | "name": "[variables('availabilitySetName')]", 90 | "location": "[resourceGroup().location]", 91 | "properties": { 92 | "platformUpdateDomainCount": "1", 93 | "platformFaultDomainCount": "1", 94 | "internalData": { 95 | "pinnedFabricCluster": "[parameters('clusterName')]" 96 | } 97 | }, 98 | "tags": { 99 | "TipNode.SessionId": "[parameters('tipNodeSessionId')]" 100 | }, 101 | "sku": { 102 | "name": "aligned" 103 | } 104 | }, 105 | {{end}} 106 | { 107 | "type": "Microsoft.Compute/virtualMachines", 108 | "apiVersion": "2021-07-01", 109 | "name": "[parameters('vmName')]", 110 | "location": "[resourceGroup().location]", 111 | "dependsOn": [ 112 | {{if HasTipNodeSession}} 113 | "[variables('availabilitySetName')]", 114 | {{end}} 115 | "[concat('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]" 116 | ], 117 | "properties": { 118 | "hardwareProfile": { 119 | "vmSize": "[parameters('vmSize')]" 120 | }, 121 | "osProfile": { 122 | "computerName": "[parameters('vmName')]", 123 | "adminUsername": "[parameters('adminUsername')]", 124 | "adminPassword": "[parameters('adminPasswordOrKey')]", 125 | "linuxConfiguration": "[if(equals(parameters('authenticationType'), 'password'), json('null'), variables('linuxConfiguration'))]", 126 | "windowsConfiguration": "[if(variables('isWindows'), variables('windowsConfiguration'), json('null'))]" 127 | }, 128 | "securityProfile": "[if(variables('isMemoryUnencrypted'), json('null'), variables('vmSecurityProfile'))]", 129 | "storageProfile": { 130 | "osDisk": { 131 | "createOption": "fromImage", 132 | "managedDisk": "[if(variables('isMemoryUnencrypted'), variables('vmStorageProfileManagedDisk'), variables('vmStorageProfileManagedDiskEncrypted'))]" 133 | }, 134 | "imageReference": "[variables('imageReference')]" 135 | }, 136 | "networkProfile": { 137 | "networkInterfaces": [ 138 | { 139 | "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]" 140 | } 141 | ] 142 | }, 143 | {{if HasTipNodeSession}} 144 | "availabilitySet": { 145 | "id": "[resourceId('Microsoft.Compute/availabilitySets', variables('availabilitySetName'))]" 146 | }, 147 | {{end}} 148 | "diagnosticsProfile": { 149 | "bootDiagnostics": { 150 | "enabled": "[equals(parameters('bootDiagnostics'), 'true')]", 151 | "storageUri": "[if(equals(parameters('bootDiagnostics'), 'true'), reference(resourceId(parameters('diagnosticsStorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', parameters('diagnosticsStorageAccountName')), '2018-02-01').primaryEndpoints['blob'], json('null'))]" 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pkg/engine/template_generator.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "runtime/debug" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/microsoft/acc-vm-engine/pkg/api" 13 | "github.com/microsoft/acc-vm-engine/pkg/helpers" 14 | ) 15 | 16 | var ( 17 | baseFile = "base.t" 18 | templateFiles = []string{baseFile, "outputs.t", "params.t", "resources.t", "vars.t"} 19 | ) 20 | 21 | // TemplateGenerator represents the object that performs the template generation. 22 | type TemplateGenerator struct { 23 | } 24 | 25 | // InitializeTemplateGenerator creates a new template generator object 26 | func InitializeTemplateGenerator(vmconfig api.VMConfigurator) (*TemplateGenerator, error) { 27 | t := &TemplateGenerator{} 28 | 29 | if err := t.verifyFiles(vmconfig); err != nil { 30 | return nil, err 31 | } 32 | 33 | return t, nil 34 | } 35 | 36 | // GenerateTemplate generates the template from the API Model 37 | func (t *TemplateGenerator) GenerateTemplate(vm *api.APIModel, generatorCode string) (templateRaw string, parametersRaw string, err error) { 38 | // named return values are used in order to set err in case of a panic 39 | templateRaw = "" 40 | parametersRaw = "" 41 | err = nil 42 | 43 | var templ *template.Template 44 | 45 | properties := vm.Properties 46 | 47 | setPropertiesDefaults(vm) 48 | 49 | templ = template.New("vm template").Funcs(t.getTemplateFuncMap(vm)) 50 | 51 | for _, file := range templateFiles { 52 | bytes, e := Asset(file) 53 | if e != nil { 54 | err = fmt.Errorf("Error reading file %s, Error: %s", file, e.Error()) 55 | return templateRaw, parametersRaw, err 56 | } 57 | if _, err = templ.New(file).Parse(string(bytes)); err != nil { 58 | return templateRaw, parametersRaw, err 59 | } 60 | } 61 | // template generation may have panics in the called functions. This catches those panics 62 | // and ensures the panic is returned as an error 63 | defer func() { 64 | if r := recover(); r != nil { 65 | s := debug.Stack() 66 | err = fmt.Errorf("%v - %s", r, s) 67 | 68 | // invalidate the template and the parameters 69 | templateRaw = "" 70 | parametersRaw = "" 71 | } 72 | }() 73 | 74 | var b bytes.Buffer 75 | if err = templ.ExecuteTemplate(&b, baseFile, properties); err != nil { 76 | return templateRaw, parametersRaw, err 77 | } 78 | templateRaw = b.String() 79 | 80 | var parametersMap paramsMap 81 | if parametersMap, err = getParameters(vm, generatorCode); err != nil { 82 | return templateRaw, parametersRaw, err 83 | } 84 | 85 | var parameterBytes []byte 86 | if parameterBytes, err = helpers.JSONMarshalIndent(parametersMap, "", " ", false); err != nil { 87 | return templateRaw, parametersRaw, err 88 | } 89 | parametersRaw = string(parameterBytes) 90 | 91 | return templateRaw, parametersRaw, err 92 | } 93 | 94 | func (t *TemplateGenerator) verifyFiles(vmconfig api.VMConfigurator) error { 95 | for _, file := range templateFiles { 96 | if _, err := Asset(file); err != nil { 97 | return fmt.Errorf("template file %s does not exist", file) 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | // getTemplateFuncMap returns all functions used in template generation 104 | func (t *TemplateGenerator) getTemplateFuncMap(vm *api.APIModel) template.FuncMap { 105 | return template.FuncMap{ 106 | "RequiresFakeAgentOutput": func() bool { 107 | return false 108 | }, 109 | "IsPublic": func(ports []int) bool { 110 | return len(ports) > 0 111 | }, 112 | 113 | "IsPrivateCluster": func() bool { 114 | return false 115 | }, 116 | "GetLBRules": func(name string, ports []int) string { 117 | return getLBRules(name, ports) 118 | }, 119 | "GetProbes": func(ports []int) string { 120 | return getProbes(ports) 121 | }, 122 | "GetSecurityRules": func(ports []int) string { 123 | return getSecurityRules(ports) 124 | }, 125 | "GetLinuxPublicKeys": func() string { 126 | if vm.Properties.LinuxProfile == nil { 127 | return `"json('null')"` 128 | } 129 | keyTempl := ` { 130 | "keyData": "%s", 131 | "path": "/home/%s/.ssh/authorized_keys" 132 | }` 133 | keyData := make([]string, len(vm.Properties.LinuxProfile.SSHPubKeys)) 134 | for i, key := range vm.Properties.LinuxProfile.SSHPubKeys { 135 | keyData[i] = fmt.Sprintf(keyTempl, key.KeyData, vm.Properties.LinuxProfile.AdminUsername) 136 | } 137 | sshTempl := `{ 138 | "publicKeys": [ 139 | %s 140 | ] 141 | }` 142 | return fmt.Sprintf(sshTempl, strings.Join(keyData, ",\n")) 143 | }, 144 | "GetVMSizes": func() string { 145 | return api.GetVMSizes(vm.VMConfigurator) 146 | }, 147 | "GetOsDiskTypes": func() string { 148 | return api.GetOsDiskTypes(vm.VMConfigurator) 149 | }, 150 | "GetDataDisks": func(p *api.Properties) string { 151 | return getDataDisks(p.VMProfile) 152 | }, 153 | "HasSecurityProfile": func() bool { 154 | return (vm.Properties.VMProfile.SecurityProfile != nil) 155 | }, 156 | "GetVMSecurityType": func() string { 157 | switch vm.VMCategory { 158 | case api.TVM: 159 | return "" 160 | case api.CVM: 161 | return "ConfidentialVM" 162 | default: 163 | return "None" 164 | } 165 | }, 166 | "HasTipNodeSession": func() bool { 167 | return len(vm.Properties.VMProfile.TipNodeSessionID) > 0 168 | }, 169 | "Base64": func(s string) string { 170 | return base64.StdEncoding.EncodeToString([]byte(s)) 171 | }, 172 | "WrapAsVariable": func(s string) string { 173 | return fmt.Sprintf("',variables('%s'),'", s) 174 | }, 175 | "WrapAsVerbatim": func(s string) string { 176 | return fmt.Sprintf("',%s,'", s) 177 | }, 178 | "HasCustomOsImage": func() bool { 179 | return vm.Properties.VMProfile.HasCustomOsImage() 180 | }, 181 | "HasDNSName": func(p *api.Properties) bool { 182 | return p.VMProfile.HasDNSName 183 | }, 184 | // inspired by http://stackoverflow.com/questions/18276173/calling-a-template-with-several-pipeline-parameters/18276968#18276968 185 | "dict": func(values ...interface{}) (map[string]interface{}, error) { 186 | if len(values)%2 != 0 { 187 | return nil, errors.New("invalid dict call") 188 | } 189 | dict := make(map[string]interface{}, len(values)/2) 190 | for i := 0; i < len(values); i += 2 { 191 | key, ok := values[i].(string) 192 | if !ok { 193 | return nil, errors.New("dict keys must be strings") 194 | } 195 | dict[key] = values[i+1] 196 | } 197 | return dict, nil 198 | }, 199 | "loop": func(min, max int) []int { 200 | var s []int 201 | for i := min; i <= max; i++ { 202 | s = append(s, i) 203 | } 204 | return s 205 | }, 206 | "subtract": func(a, b int) int { 207 | return a - b 208 | }, 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pkg/api/validate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/microsoft/acc-vm-engine/pkg/api/common" 9 | validator "gopkg.in/go-playground/validator.v9" 10 | ) 11 | 12 | var ( 13 | validate *validator.Validate 14 | keyvaultIDRegex *regexp.Regexp 15 | labelValueRegex *regexp.Regexp 16 | labelKeyRegex *regexp.Regexp 17 | ) 18 | 19 | const ( 20 | labelKeyPrefixMaxLength = 253 21 | labelValueFormat = "^([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$" 22 | labelKeyFormat = "^(([a-zA-Z0-9-]+[.])*[a-zA-Z0-9-]+[/])?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$" 23 | ) 24 | 25 | func init() { 26 | validate = validator.New() 27 | keyvaultIDRegex = regexp.MustCompile(`^/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.KeyVault/vaults/[^/\s]+$`) 28 | labelValueRegex = regexp.MustCompile(labelValueFormat) 29 | labelKeyRegex = regexp.MustCompile(labelKeyFormat) 30 | } 31 | 32 | // Validate implements APIObject 33 | func (p *Properties) Validate(vmconf VMConfigurator, isUpdate bool) error { 34 | if e := validate.Struct(p); e != nil { 35 | return handleValidationErrors(e.(validator.ValidationErrors)) 36 | } 37 | if e := p.validateVMProfile(vmconf); e != nil { 38 | return e 39 | } 40 | if e := p.validateVnetProfile(); e != nil { 41 | return e 42 | } 43 | if e := p.validateDiagnosticsProfile(); e != nil { 44 | return e 45 | } 46 | return nil 47 | } 48 | 49 | func handleValidationErrors(e validator.ValidationErrors) error { 50 | // Override any version specific validation error message 51 | // common.HandleValidationErrors if the validation error message is general 52 | return common.HandleValidationErrors(e) 53 | } 54 | 55 | func (p *Properties) validateVMProfile(vmconf VMConfigurator) error { 56 | vm := p.VMProfile 57 | if vm == nil { 58 | return fmt.Errorf("VMProfile is not specified") 59 | } 60 | 61 | if (vm.SecurityProfile != nil) { 62 | if (vm.SecurityProfile.SecureBoot != "true") && (vm.SecurityProfile.SecureBoot != "false") && (vm.SecurityProfile.SecureBoot != "none"){ 63 | return fmt.Errorf("Invalid Entry! Only the values \"true\", \"false\" and \"none\" are allowed for secure_boot_enabled") 64 | } 65 | if (vm.SecurityProfile.VTPM != "true") && (vm.SecurityProfile.VTPM != "false") { 66 | return fmt.Errorf("Invalid Entry! Only the values \"true\" and \"false\" are allowed for VTPM") 67 | } 68 | if (vm.SecurityProfile.SecureBoot == "none") && (vm.SecurityProfile.VTPM == "true") { 69 | return fmt.Errorf("Invalid Entry! vTPM cannot be \"true\" when secure-boot is \"none\"") 70 | } 71 | } 72 | if (len(vm.TipNodeSessionID) == 0 && len(vm.ClusterName) != 0) || (len(vm.TipNodeSessionID) != 0 && len(vm.ClusterName) == 0) { 73 | return fmt.Errorf("Must specify either both 'tip_node_session_id' and 'cluster_name', or neither") 74 | } 75 | if len(vm.OSDiskType) > 0 { 76 | found := false 77 | for _, t := range vmconf.AllowedOsDiskTypes() { 78 | if t == vm.OSDiskType { 79 | found = true 80 | break 81 | } 82 | } 83 | if !found { 84 | return fmt.Errorf("OS disk type '%s' is not included in supported [%s]", vm.OSDiskType, strings.Join(vmconf.AllowedOsDiskTypes(), ",")) 85 | } 86 | } 87 | if len(vm.Ports) > 0 { 88 | if e := validateUniquePorts(vm.Ports, vm.Name); e != nil { 89 | return e 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | func validateLinuxProfile(p *LinuxProfile) error { 96 | if p == nil { 97 | return fmt.Errorf("LinuxProfile cannot be empty") 98 | } 99 | if len(p.AdminUsername) == 0 { 100 | return fmt.Errorf("LinuxProfile.AdminUsername cannot be empty") 101 | } 102 | if len(p.AdminPasswordOrKey) > 0 && len(p.SSHPubKeys) > 0 { 103 | return fmt.Errorf("AdminPassword and SSH public keys are mutually exclusive") 104 | } 105 | if len(p.AdminPasswordOrKey) == 0 && len(p.SSHPubKeys) == 0 { 106 | return fmt.Errorf("Must specify either AdminPassword or SSH public keys") 107 | } 108 | for i, key := range p.SSHPubKeys { 109 | if key == nil || len(key.KeyData) == 0 { 110 | return fmt.Errorf("SSH public key #%d cannot be empty", i) 111 | } 112 | } 113 | return nil 114 | } 115 | 116 | func validateWindowsProfile(p *WindowsProfile) error { 117 | if p == nil { 118 | return fmt.Errorf("WindowsProfile cannot be empty") 119 | } 120 | if e := validate.Var(p.AdminUsername, "required"); e != nil { 121 | return fmt.Errorf("WindowsProfile.AdminUsername cannot be empty") 122 | } 123 | if e := validate.Var(p.AdminPasswordOrKey, "required"); e != nil { 124 | return fmt.Errorf("WindowsProfile.AdminPassword cannot be empty") 125 | } 126 | return nil 127 | } 128 | 129 | func validateOSImage(p *OSImage) error { 130 | if p == nil { 131 | return nil 132 | } 133 | if len(p.URL) > 0 { 134 | if len(p.Publisher) > 0 || len(p.Offer) > 0 || len(p.SKU) > 0 || len(p.Version) > 0 { 135 | return fmt.Errorf("OS image URL and Publisher/Offer/SKU are mutually exclusive") 136 | } 137 | } else { 138 | if len(p.Publisher) == 0 { 139 | return fmt.Errorf("OS image Publisher is not set") 140 | } 141 | if len(p.Offer) == 0 { 142 | return fmt.Errorf("OS image Offer is not set") 143 | } 144 | if len(p.SKU) == 0 { 145 | return fmt.Errorf("OS image SKU is not set") 146 | } 147 | // version is optional 148 | } 149 | return nil 150 | } 151 | 152 | func (p *Properties) validateDiagnosticsProfile() error { 153 | if p.DiagnosticsProfile == nil || !p.DiagnosticsProfile.Enabled { 154 | return nil 155 | } 156 | if len(p.DiagnosticsProfile.StorageAccountName) == 0 { 157 | return fmt.Errorf("DiagnosticsProfile.StorageAccountName cannot be empty string") 158 | } 159 | return nil 160 | } 161 | 162 | func (p *Properties) validateVnetProfile() error { 163 | h := p.VnetProfile 164 | if h == nil { 165 | return nil 166 | } 167 | // existing vnet is uniquely defined by resource group, vnet name, and subnet name 168 | if len(h.VnetResourceGroup) > 0 { 169 | if len(h.VirtualNetworkName) == 0 { 170 | return fmt.Errorf("vnetProfile.vnetName cannot be empty for existing vnet") 171 | } 172 | if len(h.SubnetName) == 0 { 173 | return fmt.Errorf("vnetProfile.subnetName cannot be empty for existing vnet") 174 | } 175 | if len(h.VnetAddress) > 0 { 176 | return fmt.Errorf("vnetProfile.VnetResourceGroup and vnetProfile.vnetAddress are mutually exclusive") 177 | } 178 | if len(h.SubnetAddress) > 0 { 179 | return fmt.Errorf("vnetProfile.VnetResourceGroup and vnetProfile.subnetAddress are mutually exclusive") 180 | } 181 | } 182 | return nil 183 | } 184 | 185 | func validateUniquePorts(ports []int, name string) error { 186 | portMap := make(map[int]bool) 187 | for _, port := range ports { 188 | if _, ok := portMap[port]; ok { 189 | return fmt.Errorf("VM '%s' has duplicate port '%d', ports must be unique", name, port) 190 | } 191 | portMap[port] = true 192 | } 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /pkg/api/common/versions_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetVersionsGt(t *testing.T) { 8 | versions := []string{"1.1.0-rc.1", "1.2.0-rc.1", "1.2.0", "1.2.1"} 9 | expected := []string{"1.1.0-rc.1", "1.2.0-rc.1", "1.2.0", "1.2.1"} 10 | expectedMap := map[string]bool{ 11 | "1.1.0-rc.1": true, 12 | "1.2.0-rc.1": true, 13 | "1.2.0": true, 14 | "1.2.1": true, 15 | } 16 | v := GetVersionsGt(versions, "1.1.0-alpha.1", false, true) 17 | errStr := "GetVersionsGt returned an unexpected list of strings" 18 | if len(v) != len(expected) { 19 | t.Errorf(errStr) 20 | } 21 | for _, ver := range v { 22 | if !expectedMap[ver] { 23 | t.Errorf(errStr) 24 | } 25 | } 26 | 27 | versions = []string{"1.1.0", "1.2.0", "1.2.1"} 28 | expected = []string{"1.1.0", "1.2.0", "1.2.1"} 29 | expectedMap = map[string]bool{ 30 | "1.1.0": true, 31 | "1.2.0": true, 32 | "1.2.1": true, 33 | } 34 | v = GetVersionsGt(versions, "1.1.0", true, false) 35 | if len(v) != len(expected) { 36 | t.Errorf(errStr) 37 | } 38 | for _, ver := range v { 39 | if !expectedMap[ver] { 40 | t.Errorf(errStr) 41 | } 42 | } 43 | } 44 | 45 | func TestGetVersionsLt(t *testing.T) { 46 | versions := []string{"1.1.0", "1.2.0-rc.1", "1.2.0", "1.2.1"} 47 | expected := []string{"1.1.0", "1.2.0"} 48 | // less than comparisons exclude pre-release versions from the result 49 | expectedMap := map[string]bool{ 50 | "1.1.0": true, 51 | "1.2.0": true, 52 | } 53 | v := GetVersionsLt(versions, "1.2.1", false, false) 54 | errStr := "GetVersionsLt returned an unexpected list of strings" 55 | if len(v) != len(expected) { 56 | t.Errorf(errStr) 57 | } 58 | for _, ver := range v { 59 | if !expectedMap[ver] { 60 | t.Errorf(errStr) 61 | } 62 | } 63 | 64 | versions = []string{"1.1.0", "1.2.0", "1.2.1"} 65 | expected = []string{"1.1.0", "1.2.0", "1.2.1"} 66 | expectedMap = map[string]bool{ 67 | "1.1.0": true, 68 | "1.2.0": true, 69 | "1.2.1": true, 70 | } 71 | v = GetVersionsLt(versions, "1.2.1", true, false) 72 | if len(v) != len(expected) { 73 | t.Errorf(errStr) 74 | } 75 | for _, ver := range v { 76 | if !expectedMap[ver] { 77 | t.Errorf(errStr) 78 | } 79 | } 80 | } 81 | 82 | func TestGetVersionsBetween(t *testing.T) { 83 | versions := []string{"1.1.0", "1.2.0", "1.2.1"} 84 | expected := []string{"1.2.0"} 85 | expectedMap := map[string]bool{ 86 | "1.2.0": true, 87 | } 88 | v := GetVersionsBetween(versions, "1.1.0", "1.2.1", false, false) 89 | errStr := "GetVersionsBetween returned an unexpected list of strings" 90 | if len(v) != len(expected) { 91 | t.Errorf(errStr) 92 | } 93 | for _, ver := range v { 94 | if !expectedMap[ver] { 95 | t.Errorf(errStr) 96 | } 97 | } 98 | 99 | versions = []string{"1.1.0", "1.2.0", "1.2.1"} 100 | expected = []string{"1.1.0", "1.2.0", "1.2.1"} 101 | expectedMap = map[string]bool{ 102 | "1.1.0": true, 103 | "1.2.0": true, 104 | "1.2.1": true, 105 | } 106 | v = GetVersionsBetween(versions, "1.1.0", "1.2.1", true, false) 107 | if len(v) != len(expected) { 108 | t.Errorf(errStr) 109 | } 110 | for _, ver := range v { 111 | if !expectedMap[ver] { 112 | t.Errorf(errStr) 113 | } 114 | } 115 | 116 | versions = []string{"1.9.6", "1.10.0-beta.2", "1.10.0-beta.4", "1.10.0-rc.1"} 117 | expected = []string{"1.10.0-beta.2", "1.10.0-beta.4", "1.10.0-rc.1"} 118 | expectedMap = map[string]bool{ 119 | "1.10.0-beta.2": true, 120 | "1.10.0-beta.4": true, 121 | "1.10.0-rc.1": true, 122 | } 123 | v = GetVersionsBetween(versions, "1.9.6", "1.11.0", false, true) 124 | if len(v) != len(expected) { 125 | t.Errorf(errStr) 126 | } 127 | for _, ver := range v { 128 | if !expectedMap[ver] { 129 | t.Errorf(errStr) 130 | } 131 | } 132 | v = GetVersionsBetween(versions, "1.9.6", "1.11.0", false, false) 133 | if len(v) != 0 { 134 | t.Errorf(errStr) 135 | } 136 | 137 | versions = []string{"1.9.6", "1.10.0-beta.2", "1.10.0-beta.4", "1.10.0-rc.1"} 138 | expected = []string{"1.10.0-beta.4", "1.10.0-rc.1"} 139 | expectedMap = map[string]bool{ 140 | "1.10.0-beta.4": true, 141 | "1.10.0-rc.1": true, 142 | } 143 | v = GetVersionsBetween(versions, "1.10.0-beta.2", "1.12.0", false, false) 144 | if len(v) != len(expected) { 145 | t.Errorf(errStr) 146 | } 147 | for _, ver := range v { 148 | if !expectedMap[ver] { 149 | t.Errorf(errStr) 150 | } 151 | } 152 | 153 | versions = []string{"1.10.0", "1.10.0-beta.2", "1.10.0-beta.4", "1.10.0-rc.1"} 154 | v = GetVersionsBetween(versions, "1.10.0", "1.12.0", false, false) 155 | if len(v) != 0 { 156 | t.Errorf(errStr) 157 | } 158 | 159 | versions = []string{"1.9.6", "1.10.0-beta.2", "1.10.0-beta.4", "1.10.0-rc.1"} 160 | expectedMap = map[string]bool{ 161 | "1.9.6": true, 162 | "1.10.0-beta.2": true, 163 | "1.10.0-beta.4": true, 164 | "1.10.0-rc.1": true, 165 | } 166 | v = GetVersionsBetween(versions, "1.9.5", "1.12.0", false, true) 167 | if len(v) != len(versions) { 168 | t.Errorf(errStr) 169 | } 170 | for _, ver := range v { 171 | if !expectedMap[ver] { 172 | t.Errorf(errStr) 173 | } 174 | } 175 | 176 | versions = []string{"1.9.6", "1.10.0", "1.10.1", "1.10.2"} 177 | expected = []string{"1.10.0", "1.10.1", "1.10.2"} 178 | expectedMap = map[string]bool{ 179 | "1.10.0": true, 180 | "1.10.1": true, 181 | "1.10.2": true, 182 | } 183 | v = GetVersionsBetween(versions, "1.10.0-rc.1", "1.12.0", false, true) 184 | if len(v) != len(expected) { 185 | t.Errorf(errStr) 186 | } 187 | for _, ver := range v { 188 | if !expectedMap[ver] { 189 | t.Errorf(errStr) 190 | } 191 | } 192 | 193 | versions = []string{"1.11.0-alpha.1", "1.11.0-alpha.2", "1.11.0-beta.1"} 194 | expected = []string{"1.11.0-alpha.2"} 195 | expectedMap = map[string]bool{ 196 | "1.11.0-alpha.2": true, 197 | } 198 | v = GetVersionsBetween(versions, "1.11.0-alpha.1", "1.11.0-beta.1", false, true) 199 | if len(v) != len(expected) { 200 | t.Errorf(errStr) 201 | } 202 | for _, ver := range v { 203 | if !expectedMap[ver] { 204 | t.Errorf(errStr) 205 | } 206 | } 207 | 208 | versions = []string{"1.11.0-alpha.1", "1.11.0-alpha.2", "1.11.0-beta.1"} 209 | expected = []string{} 210 | expectedMap = map[string]bool{} 211 | v = GetVersionsBetween(versions, "1.11.0-beta.1", "1.12.0", false, true) 212 | if len(v) != len(expected) { 213 | t.Errorf(errStr) 214 | } 215 | for _, ver := range v { 216 | if !expectedMap[ver] { 217 | t.Errorf(errStr) 218 | } 219 | } 220 | } 221 | 222 | func TestGetLatestPatchVersion(t *testing.T) { 223 | expected := "1.1.2" 224 | version := GetLatestPatchVersion("1.1", []string{"1.1.1", expected}) 225 | if version != expected { 226 | t.Errorf("GetLatestPatchVersion returned the wrong latest version, expected %s, got %s", expected, version) 227 | } 228 | 229 | expected = "1.1.2" 230 | version = GetLatestPatchVersion("1.1", []string{"1.1.0", expected}) 231 | if version != expected { 232 | t.Errorf("GetLatestPatchVersion returned the wrong latest version, expected %s, got %s", expected, version) 233 | } 234 | 235 | expected = "1.2.0" 236 | version = GetLatestPatchVersion("1.2", []string{"1.1.0", "1.3.0", expected}) 237 | if version != expected { 238 | t.Errorf("GetLatestPatchVersion returned the wrong latest version, expected %s, got %s", expected, version) 239 | } 240 | 241 | expected = "1.2.0-rc.3" 242 | version = GetLatestPatchVersion("1.2", []string{"1.2.0-alpha.1", "1.2.0-beta.1", "1.2.0-rc.3", expected}) 243 | if version != expected { 244 | t.Errorf("GetLatestPatchVersion returned the wrong latest version, expected %s, got %s", expected, version) 245 | } 246 | 247 | expected = "" 248 | version = GetLatestPatchVersion("1.2", []string{"1.1.0", "1.1.1", "1.1.2", expected}) 249 | if version != expected { 250 | t.Errorf("GetLatestPatchVersion returned the wrong latest version, expected %s, got %s", expected, version) 251 | } 252 | } 253 | 254 | func TestGetMaxVersion(t *testing.T) { 255 | expected := "1.0.3" 256 | versions := []string{"1.0.1", "1.0.2", expected} 257 | max := GetMaxVersion(versions, false) 258 | if max != expected { 259 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 260 | } 261 | 262 | expected = "1.2.3" 263 | versions = []string{"1.0.1", "1.1.2", expected} 264 | max = GetMaxVersion(versions, false) 265 | if max != expected { 266 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 267 | } 268 | 269 | expected = "1.1.2" 270 | versions = []string{"1.0.1", expected, "1.2.3-alpha.1"} 271 | max = GetMaxVersion(versions, false) 272 | if max != expected { 273 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 274 | } 275 | 276 | expected = "1.2.3-alpha.1" 277 | versions = []string{"1.0.1", "1.1.2", expected} 278 | max = GetMaxVersion(versions, true) 279 | if max != expected { 280 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 281 | } 282 | 283 | expected = "" 284 | versions = []string{} 285 | max = GetMaxVersion(versions, false) 286 | if max != expected { 287 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 288 | } 289 | 290 | expected = "" 291 | versions = []string{} 292 | max = GetMaxVersion(versions, true) 293 | if max != expected { 294 | t.Errorf("GetMaxVersion returned the wrong max version, expected %s, got %s", expected, max) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= 2 | github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 4 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= 8 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 13 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 14 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 15 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 16 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 17 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 18 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 19 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 22 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 23 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 24 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 25 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 26 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 27 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 28 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 31 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 33 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 34 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 35 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 36 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 37 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 38 | github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= 39 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 40 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 41 | github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= 42 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 43 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 44 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 48 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 51 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 52 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 53 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 54 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 55 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 56 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= 57 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 58 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= 61 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 62 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 70 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 73 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 77 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 78 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 79 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 80 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 81 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 82 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 83 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 84 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 85 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 86 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 88 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 90 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 91 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 92 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 93 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 94 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 95 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 99 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | -------------------------------------------------------------------------------- /pkg/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "text/template" 16 | 17 | "github.com/ghodss/yaml" 18 | "github.com/microsoft/acc-vm-engine/pkg/api" 19 | ) 20 | 21 | var keyvaultSecretPathRe *regexp.Regexp 22 | 23 | func init() { 24 | keyvaultSecretPathRe = regexp.MustCompile(`^(/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.KeyVault/vaults/\S+)/secrets/([^/\s]+)(/(\S+))?$`) 25 | } 26 | 27 | func generateIPList(count int, firstAddr string) []string { 28 | ipaddr := net.ParseIP(firstAddr).To4() 29 | if ipaddr == nil { 30 | panic(fmt.Sprintf("IPAddr '%s' is an invalid IP address", firstAddr)) 31 | } 32 | ret := make([]string, count) 33 | for i := 0; i < count; i++ { 34 | ret[i] = fmt.Sprintf("%d.%d.%d.%d", ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3]+byte(i)) 35 | } 36 | return ret 37 | } 38 | 39 | func addValue(m paramsMap, k string, v interface{}) { 40 | m[k] = paramsMap{ 41 | "value": v, 42 | } 43 | } 44 | 45 | // getStorageAccountType returns the support managed disk storage tier for a give VM size 46 | func getStorageAccountType(sizeName string) (string, error) { 47 | spl := strings.Split(sizeName, "_") 48 | if len(spl) < 2 { 49 | return "", fmt.Errorf("Invalid sizeName: %s", sizeName) 50 | } 51 | capability := spl[1] 52 | if strings.Contains(strings.ToLower(capability), "s") { 53 | return "Premium_LRS", nil 54 | } 55 | return "Standard_LRS", nil 56 | } 57 | 58 | func getLBRule(name string, port int) string { 59 | return fmt.Sprintf(` { 60 | "name": "LBRule%d", 61 | "properties": { 62 | "backendAddressPool": { 63 | "id": "[concat(variables('%sLbID'), '/backendAddressPools/', variables('%sLbBackendPoolName'))]" 64 | }, 65 | "backendPort": %d, 66 | "enableFloatingIP": false, 67 | "frontendIPConfiguration": { 68 | "id": "[variables('%sLbIPConfigID')]" 69 | }, 70 | "frontendPort": %d, 71 | "idleTimeoutInMinutes": 5, 72 | "loadDistribution": "Default", 73 | "probe": { 74 | "id": "[concat(variables('%sLbID'),'/probes/tcp%dProbe')]" 75 | }, 76 | "protocol": "tcp" 77 | } 78 | }`, port, name, name, port, name, port, name, port) 79 | } 80 | 81 | func getLBRules(name string, ports []int) string { 82 | var buf bytes.Buffer 83 | for index, port := range ports { 84 | if index > 0 { 85 | buf.WriteString(",\n") 86 | } 87 | buf.WriteString(getLBRule(name, port)) 88 | } 89 | return buf.String() 90 | } 91 | 92 | func getProbe(port int) string { 93 | return fmt.Sprintf(` { 94 | "name": "tcp%dProbe", 95 | "properties": { 96 | "intervalInSeconds": "5", 97 | "numberOfProbes": "2", 98 | "port": %d, 99 | "protocol": "tcp" 100 | } 101 | }`, port, port) 102 | } 103 | 104 | func getProbes(ports []int) string { 105 | var buf bytes.Buffer 106 | for index, port := range ports { 107 | if index > 0 { 108 | buf.WriteString(",\n") 109 | } 110 | buf.WriteString(getProbe(port)) 111 | } 112 | return buf.String() 113 | } 114 | 115 | func getSecurityRule(port int, portIndex int) string { 116 | // BaseLBPriority specifies the base lb priority. 117 | BaseLBPriority := 200 118 | return fmt.Sprintf(` { 119 | "name": "Allow_%d", 120 | "properties": { 121 | "access": "Allow", 122 | "description": "Allow traffic from the Internet to port %d", 123 | "destinationAddressPrefix": "*", 124 | "destinationPortRange": "%d", 125 | "direction": "Inbound", 126 | "priority": %d, 127 | "protocol": "*", 128 | "sourceAddressPrefix": "Internet", 129 | "sourcePortRange": "*" 130 | } 131 | }`, port, port, port, BaseLBPriority+portIndex) 132 | } 133 | 134 | func getDataDisks(a *api.VMProfile) string { 135 | if !a.HasDisks() { 136 | return "" 137 | } 138 | var buf bytes.Buffer 139 | buf.WriteString("\"dataDisks\": [\n") 140 | managedDataDisks := ` { 141 | "diskSizeGB": "%d", 142 | "lun": %d, 143 | "caching": "ReadOnly", 144 | "createOption": "Empty" 145 | }` 146 | for i, diskSize := range a.DiskSizesGB { 147 | if i > 0 { 148 | buf.WriteString(",\n") 149 | } 150 | buf.WriteString(fmt.Sprintf(managedDataDisks, diskSize, i)) 151 | } 152 | buf.WriteString("\n ],") 153 | return buf.String() 154 | } 155 | 156 | func getSecurityRules(ports []int) string { 157 | var buf bytes.Buffer 158 | for index, port := range ports { 159 | if index > 0 { 160 | buf.WriteString(",\n") 161 | } 162 | buf.WriteString(getSecurityRule(port, index)) 163 | } 164 | return buf.String() 165 | } 166 | 167 | // getSingleLineForTemplate returns the file as a single line for embedding in an arm template 168 | func (t *TemplateGenerator) getSingleLineForTemplate(textFilename string, vm *api.APIModel, profile interface{}) (string, error) { 169 | b, err := Asset(textFilename) 170 | if err != nil { 171 | return "", fmt.Errorf("yaml file %s does not exist", textFilename) 172 | } 173 | 174 | // use go templates to process the text filename 175 | templ := template.New("customdata template").Funcs(t.getTemplateFuncMap(vm)) 176 | if _, err = templ.New(textFilename).Parse(string(b)); err != nil { 177 | return "", fmt.Errorf("error parsing file %s: %v", textFilename, err) 178 | } 179 | 180 | var buffer bytes.Buffer 181 | if err = templ.ExecuteTemplate(&buffer, textFilename, profile); err != nil { 182 | return "", fmt.Errorf("error executing template for file %s: %v", textFilename, err) 183 | } 184 | expandedTemplate := buffer.String() 185 | 186 | textStr := escapeSingleLine(string(expandedTemplate)) 187 | 188 | return textStr, nil 189 | } 190 | 191 | func escapeSingleLine(escapedStr string) string { 192 | // template.JSEscapeString leaves undesirable chars that don't work with pretty print 193 | escapedStr = strings.Replace(escapedStr, "\\", "\\\\", -1) 194 | escapedStr = strings.Replace(escapedStr, "\r\n", "\\n", -1) 195 | escapedStr = strings.Replace(escapedStr, "\n", "\\n", -1) 196 | escapedStr = strings.Replace(escapedStr, "\"", "\\\"", -1) 197 | return escapedStr 198 | } 199 | 200 | // getBase64CustomScript will return a base64 of the CSE 201 | func getBase64CustomScript(csFilename string) string { 202 | b, err := Asset(csFilename) 203 | if err != nil { 204 | // this should never happen and this is a bug 205 | panic(fmt.Sprintf("BUG: %s", err.Error())) 206 | } 207 | // translate the parameters 208 | csStr := string(b) 209 | csStr = strings.Replace(csStr, "\r\n", "\n", -1) 210 | return getBase64CustomScriptFromStr(csStr) 211 | } 212 | 213 | // getBase64CustomScript will return a base64 of the CSE 214 | func getBase64CustomScriptFromStr(str string) string { 215 | var gzipB bytes.Buffer 216 | w := gzip.NewWriter(&gzipB) 217 | w.Write([]byte(str)) 218 | w.Close() 219 | return base64.StdEncoding.EncodeToString(gzipB.Bytes()) 220 | } 221 | 222 | func getProvisionScript(script string, replaceMap map[string]string) string { 223 | // add the provision script 224 | bp, err := Asset(script) 225 | if err != nil { 226 | panic(fmt.Sprintf("BUG: %s", err.Error())) 227 | } 228 | 229 | pScript := string(bp) 230 | if strings.Contains(pScript, "'") { 231 | panic(fmt.Sprintf("BUG: %s may not contain character '", script)) 232 | } 233 | 234 | for k, v := range replaceMap { 235 | pScript = strings.Replace(pScript, k, v, -1) 236 | } 237 | return strings.Replace(strings.Replace(pScript, "\r\n", "\n", -1), "\n", "\n\n ", -1) 238 | } 239 | 240 | // getSingleLineCustomData returns the file as a single line for embedding in an arm template 241 | func getSingleLineCustomData(yamlFilename string, replaceMap map[string]string) string { 242 | b, err := Asset(yamlFilename) 243 | if err != nil { 244 | panic(fmt.Sprintf("BUG getting yaml custom data file: %s", err.Error())) 245 | } 246 | yamlStr := string(b) 247 | for k, v := range replaceMap { 248 | yamlStr = strings.Replace(yamlStr, k, v, -1) 249 | } 250 | 251 | // convert to json 252 | jsonBytes, err4 := yaml.YAMLToJSON([]byte(yamlStr)) 253 | if err4 != nil { 254 | panic(fmt.Sprintf("BUG: %s", err4.Error())) 255 | } 256 | yamlStr = string(jsonBytes) 257 | 258 | // convert to one line 259 | yamlStr = strings.Replace(yamlStr, "\\", "\\\\", -1) 260 | yamlStr = strings.Replace(yamlStr, "\r\n", "\\n", -1) 261 | yamlStr = strings.Replace(yamlStr, "\n", "\\n", -1) 262 | yamlStr = strings.Replace(yamlStr, "\"", "\\\"", -1) 263 | 264 | // variable replacement 265 | rVariable, e1 := regexp.Compile("{{{([^}]*)}}}") 266 | if e1 != nil { 267 | panic(fmt.Sprintf("BUG: %s", e1.Error())) 268 | } 269 | yamlStr = rVariable.ReplaceAllString(yamlStr, "',variables('$1'),'") 270 | 271 | return yamlStr 272 | } 273 | 274 | // getLinkedTemplateTextForURL returns the string data from 275 | // template-link.json in the following directory: 276 | // extensionsRootURL/extensions/extensionName/version 277 | // It returns an error if the extension cannot be found 278 | // or loaded. getLinkedTemplateTextForURL provides the ability 279 | // to pass a root extensions url for testing 280 | func getLinkedTemplateTextForURL1(rootURL, orchestrator, extensionName, version, query string) (string, error) { 281 | supportsExtension, err := orchestratorSupportsExtension(rootURL, orchestrator, extensionName, version, query) 282 | if !supportsExtension { 283 | return "", fmt.Errorf("Extension not supported for orchestrator. Error: %s", err) 284 | } 285 | 286 | templateLinkBytes, err := getExtensionResource(rootURL, extensionName, version, "template-link.json", query) 287 | if err != nil { 288 | return "", err 289 | } 290 | 291 | return string(templateLinkBytes), nil 292 | } 293 | 294 | func orchestratorSupportsExtension(rootURL, orchestrator, extensionName, version, query string) (bool, error) { 295 | orchestratorBytes, err := getExtensionResource(rootURL, extensionName, version, "supported-orchestrators.json", query) 296 | if err != nil { 297 | return false, err 298 | } 299 | 300 | var supportedOrchestrators []string 301 | err = json.Unmarshal(orchestratorBytes, &supportedOrchestrators) 302 | if err != nil { 303 | return false, fmt.Errorf("Unable to parse supported-orchestrators.json for Extension %s Version %s", extensionName, version) 304 | } 305 | 306 | if !stringInSlice(orchestrator, supportedOrchestrators) { 307 | return false, fmt.Errorf("Orchestrator: %s not in list of supported orchestrators for Extension: %s Version %s", orchestrator, extensionName, version) 308 | } 309 | 310 | return true, nil 311 | } 312 | 313 | func getExtensionResource(rootURL, extensionName, version, fileName, query string) ([]byte, error) { 314 | requestURL := getExtensionURL(rootURL, extensionName, version, fileName, query) 315 | 316 | res, err := http.Get(requestURL) 317 | if err != nil { 318 | return nil, fmt.Errorf("Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s Error: %s", extensionName, version, fileName, requestURL, err) 319 | } 320 | 321 | defer res.Body.Close() 322 | 323 | if res.StatusCode != 200 { 324 | return nil, fmt.Errorf("Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s StatusCode: %s: Status: %s", extensionName, version, fileName, requestURL, strconv.Itoa(res.StatusCode), res.Status) 325 | } 326 | 327 | body, err := ioutil.ReadAll(res.Body) 328 | if err != nil { 329 | return nil, fmt.Errorf("Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s Error: %s", extensionName, version, fileName, requestURL, err) 330 | } 331 | 332 | return body, nil 333 | } 334 | 335 | func getExtensionURL(rootURL, extensionName, version, fileName, query string) string { 336 | extensionsDir := "extensions" 337 | if !strings.HasSuffix(rootURL, "/") { 338 | rootURL = rootURL + "/" 339 | } 340 | url := rootURL + extensionsDir + "/" + extensionName + "/" + version + "/" + fileName 341 | if query != "" { 342 | url += "?" + query 343 | } 344 | return url 345 | } 346 | 347 | func stringInSlice(a string, list []string) bool { 348 | for _, b := range list { 349 | if b == a { 350 | return true 351 | } 352 | } 353 | return false 354 | } 355 | --------------------------------------------------------------------------------