package vmClient import ( "bytes" "crypto/sha1" "encoding/base64" "encoding/json" "encoding/pem" "encoding/xml" "errors" "fmt" "io/ioutil" "strings" "time" "unicode" azure "github.com/MSOpenTech/azure-sdk-for-go" "github.com/MSOpenTech/azure-sdk-for-go/clients/imageClient" "github.com/MSOpenTech/azure-sdk-for-go/clients/locationClient" "github.com/MSOpenTech/azure-sdk-for-go/clients/storageServiceClient" ) const ( azureXmlns = "http://schemas.microsoft.com/windowsazure" azureDeploymentListURL = "services/hostedservices/%s/deployments" azureHostedServiceListURL = "services/hostedservices" deleteAzureHostedServiceURL = "services/hostedservices/%s?comp=media" azureHostedServiceAvailabilityURL = "services/hostedservices/operations/isavailable/%s" azureDeploymentURL = "services/hostedservices/%s/deployments/%s" deleteAzureDeploymentURL = "services/hostedservices/%s/deployments/%s?comp=media" azureRoleURL = "services/hostedservices/%s/deployments/%s/roles/%s" azureOperationsURL = "services/hostedservices/%s/deployments/%s/roleinstances/%s/Operations" azureCertificatListURL = "services/hostedservices/%s/certificates" azureRoleSizeListURL = "rolesizes" osLinux = "Linux" osWindows = "Windows" dockerPublicConfigVersion = 2 provisioningConfDoesNotExistsError = "You should set azure VM provisioning config first" invalidCertExtensionError = "Certificate %s is invalid. Please specify %s certificate." invalidOSError = "You must specify correct OS param. Valid values are 'Linux' and 'Windows'" invalidDnsLengthError = "The DNS name must be between 3 and 25 characters." invalidPasswordLengthError = "Password must be between 4 and 30 characters." invalidPasswordError = "Password must have at least one upper case, lower case and numeric character." invalidRoleSizeError = "Invalid role size: %s. Available role sizes: %s" ) //Region public methods starts func CreateAzureVM(azureVMConfiguration *Role, dnsName, location string) error { if azureVMConfiguration == nil { return fmt.Errorf(azure.ParamNotSpecifiedError, "azureVMConfiguration") } if len(dnsName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "dnsName") } if len(location) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "location") } err := verifyDNSname(dnsName) if err != nil { return err } requestId, err := CreateHostedService(dnsName, location) if err != nil { return err } azure.WaitAsyncOperation(requestId) if azureVMConfiguration.UseCertAuth { err = uploadServiceCert(dnsName, azureVMConfiguration.CertPath) if err != nil { DeleteHostedService(dnsName) return err } } vMDeployment := createVMDeploymentConfig(azureVMConfiguration) vMDeploymentBytes, err := xml.Marshal(vMDeployment) if err != nil { DeleteHostedService(dnsName) return err } requestURL := fmt.Sprintf(azureDeploymentListURL, azureVMConfiguration.RoleName) requestId, err = azure.SendAzurePostRequest(requestURL, vMDeploymentBytes) if err != nil { DeleteHostedService(dnsName) return err } azure.WaitAsyncOperation(requestId) return nil } func CreateHostedService(dnsName, location string) (string, error) { if len(dnsName) == 0 { return "", fmt.Errorf(azure.ParamNotSpecifiedError, "dnsName") } if len(location) == 0 { return "", fmt.Errorf(azure.ParamNotSpecifiedError, "location") } err := verifyDNSname(dnsName) if err != nil { return "", err } result, reason, err := CheckHostedServiceNameAvailability(dnsName) if err != nil { return "", err } if !result { return "", fmt.Errorf("%s Hosted service name: %s", reason, dnsName) } err = locationClient.ResolveLocation(location) if err != nil { return "", err } hostedServiceDeployment := createHostedServiceDeploymentConfig(dnsName, location) hostedServiceBytes, err := xml.Marshal(hostedServiceDeployment) if err != nil { return "", err } requestURL := azureHostedServiceListURL requestId, err := azure.SendAzurePostRequest(requestURL, hostedServiceBytes) if err != nil { return "", err } return requestId, nil } func CheckHostedServiceNameAvailability(dnsName string) (bool, string, error) { if len(dnsName) == 0 { return false, "", fmt.Errorf(azure.ParamNotSpecifiedError, "dnsName") } err := verifyDNSname(dnsName) if err != nil { return false, "", err } requestURL := fmt.Sprintf(azureHostedServiceAvailabilityURL, dnsName) response, err := azure.SendAzureGetRequest(requestURL) if err != nil { return false, "", err } availabilityResponse := new(AvailabilityResponse) err = xml.Unmarshal(response, availabilityResponse) if err != nil { return false, "", err } return availabilityResponse.Result, availabilityResponse.Reason, nil } func DeleteHostedService(dnsName string) error { if len(dnsName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "dnsName") } err := verifyDNSname(dnsName) if err != nil { return err } requestURL := fmt.Sprintf(deleteAzureHostedServiceURL, dnsName) requestId, err := azure.SendAzureDeleteRequest(requestURL) if err != nil { return err } azure.WaitAsyncOperation(requestId) return nil } func CreateAzureVMConfiguration(dnsName, instanceSize, imageName, location string) (*Role, error) { if len(dnsName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "dnsName") } if len(instanceSize) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "instanceSize") } if len(imageName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "imageName") } if len(location) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "location") } err := verifyDNSname(dnsName) if err != nil { return nil, err } err = locationClient.ResolveLocation(location) if err != nil { return nil, err } err = ResolveRoleSize(instanceSize) if err != nil { return nil, err } role, err := createAzureVMRole(dnsName, instanceSize, imageName, location) if err != nil { return nil, err } return role, nil } func AddAzureLinuxProvisioningConfig(azureVMConfiguration *Role, userName, password, certPath string, sshPort int) (*Role, error) { if azureVMConfiguration == nil { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "azureVMConfiguration") } if len(userName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "userName") } configurationSets := ConfigurationSets{} provisioningConfig, err := createLinuxProvisioningConfig(azureVMConfiguration.RoleName, userName, password, certPath) if err != nil { return nil, err } configurationSets.ConfigurationSet = append(configurationSets.ConfigurationSet, provisioningConfig) networkConfig, networkErr := createNetworkConfig(osLinux, sshPort) if networkErr != nil { return nil, err } configurationSets.ConfigurationSet = append(configurationSets.ConfigurationSet, networkConfig) azureVMConfiguration.ConfigurationSets = configurationSets if len(certPath) > 0 { azureVMConfiguration.UseCertAuth = true azureVMConfiguration.CertPath = certPath } return azureVMConfiguration, nil } func SetAzureVMExtension(azureVMConfiguration *Role, name string, publisher string, version string, referenceName string, state string, publicConfigurationValue string, privateConfigurationValue string) (*Role, error) { if azureVMConfiguration == nil { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "azureVMConfiguration") } if len(name) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "name") } if len(publisher) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "publisher") } if len(version) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "version") } if len(referenceName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "referenceName") } extension := ResourceExtensionReference{} extension.Name = name extension.Publisher = publisher extension.Version = version extension.ReferenceName = referenceName extension.State = state if len(privateConfigurationValue) > 0 { privateConfig := ResourceExtensionParameter{} privateConfig.Key = "ignored" privateConfig.Value = base64.StdEncoding.EncodeToString([]byte(privateConfigurationValue)) privateConfig.Type = "Private" extension.ResourceExtensionParameterValues.ResourceExtensionParameterValue = append(extension.ResourceExtensionParameterValues.ResourceExtensionParameterValue, privateConfig) } if len(publicConfigurationValue) > 0 { publicConfig := ResourceExtensionParameter{} publicConfig.Key = "ignored" publicConfig.Value = base64.StdEncoding.EncodeToString([]byte(publicConfigurationValue)) publicConfig.Type = "Public" extension.ResourceExtensionParameterValues.ResourceExtensionParameterValue = append(extension.ResourceExtensionParameterValues.ResourceExtensionParameterValue, publicConfig) } azureVMConfiguration.ResourceExtensionReferences.ResourceExtensionReference = append(azureVMConfiguration.ResourceExtensionReferences.ResourceExtensionReference, extension) return azureVMConfiguration, nil } func SetAzureDockerVMExtension(azureVMConfiguration *Role, dockerPort int, version string) (*Role, error) { if azureVMConfiguration == nil { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "azureVMConfiguration") } if len(version) == 0 { version = "0.3" } err := addDockerPort(azureVMConfiguration.ConfigurationSets.ConfigurationSet, dockerPort) if err != nil { return nil, err } publicConfiguration, err := createDockerPublicConfig(dockerPort) if err != nil { return nil, err } privateConfiguration := "{}" azureVMConfiguration, err = SetAzureVMExtension(azureVMConfiguration, "DockerExtension", "MSOpenTech.Extensions", version, "DockerExtension", "enable", publicConfiguration, privateConfiguration) return azureVMConfiguration, nil } func GetVMDeployment(cloudserviceName, deploymentName string) (*VMDeployment, error) { if len(cloudserviceName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } deployment := new(VMDeployment) requestURL := fmt.Sprintf(azureDeploymentURL, cloudserviceName, deploymentName) response, azureErr := azure.SendAzureGetRequest(requestURL) if azureErr != nil { return nil, azureErr } err := xml.Unmarshal(response, deployment) if err != nil { return nil, err } return deployment, nil } func DeleteVMDeployment(cloudserviceName, deploymentName string) error { if len(cloudserviceName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } requestURL := fmt.Sprintf(deleteAzureDeploymentURL, cloudserviceName, deploymentName) requestId, err := azure.SendAzureDeleteRequest(requestURL) if err != nil { return err } azure.WaitAsyncOperation(requestId) return nil } func GetRole(cloudserviceName, deploymentName, roleName string) (*Role, error) { if len(cloudserviceName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } if len(roleName) == 0 { return nil, fmt.Errorf(azure.ParamNotSpecifiedError, "roleName") } role := new(Role) requestURL := fmt.Sprintf(azureRoleURL, cloudserviceName, deploymentName, roleName) response, azureErr := azure.SendAzureGetRequest(requestURL) if azureErr != nil { return nil, azureErr } err := xml.Unmarshal(response, role) if err != nil { return nil, err } return role, nil } func StartRole(cloudserviceName, deploymentName, roleName string) error { if len(cloudserviceName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } if len(roleName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "roleName") } startRoleOperation := createStartRoleOperation() startRoleOperationBytes, err := xml.Marshal(startRoleOperation) if err != nil { return err } requestURL := fmt.Sprintf(azureOperationsURL, cloudserviceName, deploymentName, roleName) requestId, azureErr := azure.SendAzurePostRequest(requestURL, startRoleOperationBytes) if azureErr != nil { return azureErr } azure.WaitAsyncOperation(requestId) return nil } func ShutdownRole(cloudserviceName, deploymentName, roleName string) error { if len(cloudserviceName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } if len(roleName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "roleName") } shutdownRoleOperation := createShutdowRoleOperation() shutdownRoleOperationBytes, err := xml.Marshal(shutdownRoleOperation) if err != nil { return err } requestURL := fmt.Sprintf(azureOperationsURL, cloudserviceName, deploymentName, roleName) requestId, azureErr := azure.SendAzurePostRequest(requestURL, shutdownRoleOperationBytes) if azureErr != nil { return azureErr } azure.WaitAsyncOperation(requestId) return nil } func RestartRole(cloudserviceName, deploymentName, roleName string) error { if len(cloudserviceName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } if len(roleName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "roleName") } restartRoleOperation := createRestartRoleOperation() restartRoleOperationBytes, err := xml.Marshal(restartRoleOperation) if err != nil { return err } requestURL := fmt.Sprintf(azureOperationsURL, cloudserviceName, deploymentName, roleName) requestId, azureErr := azure.SendAzurePostRequest(requestURL, restartRoleOperationBytes) if azureErr != nil { return azureErr } azure.WaitAsyncOperation(requestId) return nil } func DeleteRole(cloudserviceName, deploymentName, roleName string) error { if len(cloudserviceName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "cloudserviceName") } if len(deploymentName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "deploymentName") } if len(roleName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "roleName") } requestURL := fmt.Sprintf(azureRoleURL, cloudserviceName, deploymentName, roleName) requestId, azureErr := azure.SendAzureDeleteRequest(requestURL) if azureErr != nil { return azureErr } azure.WaitAsyncOperation(requestId) return nil } func GetRoleSizeList() (RoleSizeList, error) { roleSizeList := RoleSizeList{} response, err := azure.SendAzureGetRequest(azureRoleSizeListURL) if err != nil { return roleSizeList, err } err = xml.Unmarshal(response, &roleSizeList) if err != nil { return roleSizeList, err } return roleSizeList, err } func ResolveRoleSize(roleSizeName string) error { if len(roleSizeName) == 0 { return fmt.Errorf(azure.ParamNotSpecifiedError, "roleSizeName") } roleSizeList, err := GetRoleSizeList() if err != nil { return err } for _, roleSize := range roleSizeList.RoleSizes { if roleSize.Name != roleSizeName { continue } return nil } var availableSizes bytes.Buffer for _, existingSize := range roleSizeList.RoleSizes { availableSizes.WriteString(existingSize.Name + ", ") } return errors.New(fmt.Sprintf(invalidRoleSizeError, roleSizeName, strings.Trim(availableSizes.String(), ", "))) } //Region public methods ends //Region private methods starts func createStartRoleOperation() StartRoleOperation { startRoleOperation := StartRoleOperation{} startRoleOperation.OperationType = "StartRoleOperation" startRoleOperation.Xmlns = azureXmlns return startRoleOperation } func createShutdowRoleOperation() ShutdownRoleOperation { shutdownRoleOperation := ShutdownRoleOperation{} shutdownRoleOperation.OperationType = "ShutdownRoleOperation" shutdownRoleOperation.Xmlns = azureXmlns return shutdownRoleOperation } func createRestartRoleOperation() RestartRoleOperation { startRoleOperation := RestartRoleOperation{} startRoleOperation.OperationType = "RestartRoleOperation" startRoleOperation.Xmlns = azureXmlns return startRoleOperation } func createDockerPublicConfig(dockerPort int) (string, error) { config := dockerPublicConfig{DockerPort: dockerPort, Version: dockerPublicConfigVersion} configJson, err := json.Marshal(config) if err != nil { return "", err } return string(configJson), nil } func addDockerPort(configurationSets []ConfigurationSet, dockerPort int) error { if len(configurationSets) == 0 { return errors.New(provisioningConfDoesNotExistsError) } for i := 0; i < len(configurationSets); i++ { if configurationSets[i].ConfigurationSetType != "NetworkConfiguration" { continue } dockerEndpoint := createEndpoint("docker", "tcp", dockerPort, dockerPort) configurationSets[i].InputEndpoints.InputEndpoint = append(configurationSets[i].InputEndpoints.InputEndpoint, dockerEndpoint) } return nil } func createHostedServiceDeploymentConfig(dnsName, location string) HostedServiceDeployment { deployment := HostedServiceDeployment{} deployment.ServiceName = dnsName label := base64.StdEncoding.EncodeToString([]byte(dnsName)) deployment.Label = label deployment.Location = location deployment.Xmlns = azureXmlns return deployment } func createVMDeploymentConfig(role *Role) VMDeployment { deployment := VMDeployment{} deployment.Name = role.RoleName deployment.Xmlns = azureXmlns deployment.DeploymentSlot = "Production" deployment.Label = role.RoleName deployment.RoleList.Role = append(deployment.RoleList.Role, role) return deployment } func createAzureVMRole(name, instanceSize, imageName, location string) (*Role, error) { config := new(Role) config.RoleName = name config.RoleSize = instanceSize config.RoleType = "PersistentVMRole" config.ProvisionGuestAgent = true var err error config.OSVirtualHardDisk, err = createOSVirtualHardDisk(name, imageName, location) if err != nil { return nil, err } return config, nil } func createOSVirtualHardDisk(dnsName, imageName, location string) (OSVirtualHardDisk, error) { oSVirtualHardDisk := OSVirtualHardDisk{} err := imageClient.ResolveImageName(imageName) if err != nil { return oSVirtualHardDisk, err } oSVirtualHardDisk.SourceImageName = imageName oSVirtualHardDisk.MediaLink, err = getVHDMediaLink(dnsName, location) if err != nil { return oSVirtualHardDisk, err } return oSVirtualHardDisk, nil } func getVHDMediaLink(dnsName, location string) (string, error) { storageService, err := storageServiceClient.GetStorageServiceByLocation(location) if err != nil { return "", err } if storageService == nil { uuid, err := azure.NewUUID() if err != nil { return "", err } serviceName := "portalvhds" + uuid storageService, err = storageServiceClient.CreateStorageService(serviceName, location) if err != nil { return "", err } } blobEndpoint, err := storageServiceClient.GetBlobEndpoint(storageService) if err != nil { return "", err } vhdMediaLink := blobEndpoint + "vhds/" + dnsName + "-" + time.Now().Local().Format("20060102150405") + ".vhd" return vhdMediaLink, nil } func createLinuxProvisioningConfig(dnsName, userName, userPassword, certPath string) (ConfigurationSet, error) { provisioningConfig := ConfigurationSet{} disableSshPasswordAuthentication := false if len(userPassword) == 0 { disableSshPasswordAuthentication = true // We need to set dummy password otherwise azure API will throw an error userPassword = "P@ssword1" } else { err := verifyPassword(userPassword) if err != nil { return provisioningConfig, err } } provisioningConfig.DisableSshPasswordAuthentication = disableSshPasswordAuthentication provisioningConfig.ConfigurationSetType = "LinuxProvisioningConfiguration" provisioningConfig.HostName = dnsName provisioningConfig.UserName = userName provisioningConfig.UserPassword = userPassword if len(certPath) > 0 { var err error provisioningConfig.SSH, err = createSshConfig(certPath, userName) if err != nil { return provisioningConfig, err } } return provisioningConfig, nil } func uploadServiceCert(dnsName, certPath string) error { certificateConfig, err := createServiceCertDeploymentConf(certPath) if err != nil { return err } certificateConfigBytes, err := xml.Marshal(certificateConfig) if err != nil { return err } requestURL := fmt.Sprintf(azureCertificatListURL, dnsName) requestId, azureErr := azure.SendAzurePostRequest(requestURL, certificateConfigBytes) if azureErr != nil { return azureErr } err = azure.WaitAsyncOperation(requestId) return err } func createServiceCertDeploymentConf(certPath string) (ServiceCertificate, error) { certConfig := ServiceCertificate{} certConfig.Xmlns = azureXmlns data, err := ioutil.ReadFile(certPath) if err != nil { return certConfig, err } certData := base64.StdEncoding.EncodeToString(data) certConfig.Data = certData certConfig.CertificateFormat = "pfx" return certConfig, nil } func createSshConfig(certPath, userName string) (SSH, error) { sshConfig := SSH{} publicKey := PublicKey{} err := checkServiceCertExtension(certPath) if err != nil { return sshConfig, err } fingerprint, err := getServiceCertFingerprint(certPath) if err != nil { return sshConfig, err } publicKey.Fingerprint = fingerprint publicKey.Path = "/home/" + userName + "/.ssh/authorized_keys" sshConfig.PublicKeys.PublicKey = append(sshConfig.PublicKeys.PublicKey, publicKey) return sshConfig, nil } func getServiceCertFingerprint(certPath string) (string, error) { certData, readErr := ioutil.ReadFile(certPath) if readErr != nil { return "", readErr } block, rest := pem.Decode(certData) if block == nil { return "", errors.New(string(rest)) } sha1sum := sha1.Sum(block.Bytes) fingerprint := fmt.Sprintf("%X", sha1sum) return fingerprint, nil } func checkServiceCertExtension(certPath string) error { certParts := strings.Split(certPath, ".") certExt := certParts[len(certParts)-1] acceptedExtension := "pem" if certExt != acceptedExtension { return errors.New(fmt.Sprintf(invalidCertExtensionError, certPath, acceptedExtension)) } return nil } func createNetworkConfig(os string, sshPort int) (ConfigurationSet, error) { networkConfig := ConfigurationSet{} networkConfig.ConfigurationSetType = "NetworkConfiguration" var endpoint InputEndpoint if os == osLinux { endpoint = createEndpoint("ssh", "tcp", sshPort, 22) } else if os == osWindows { //!TODO add rdp endpoint } else { return networkConfig, errors.New(fmt.Sprintf(invalidOSError)) } networkConfig.InputEndpoints.InputEndpoint = append(networkConfig.InputEndpoints.InputEndpoint, endpoint) return networkConfig, nil } func createEndpoint(name string, protocol string, extertalPort int, internalPort int) InputEndpoint { endpoint := InputEndpoint{} endpoint.Name = name endpoint.Protocol = protocol endpoint.Port = extertalPort endpoint.LocalPort = internalPort return endpoint } func verifyDNSname(dns string) error { if len(dns) < 3 || len(dns) > 25 { return fmt.Errorf(invalidDnsLengthError) } return nil } func verifyPassword(password string) error { if len(password) < 4 || len(password) > 30 { return fmt.Errorf(invalidPasswordLengthError) } next: for _, classes := range map[string][]*unicode.RangeTable{ "upper case": {unicode.Upper, unicode.Title}, "lower case": {unicode.Lower}, "numeric": {unicode.Number, unicode.Digit}, } { for _, r := range password { if unicode.IsOneOf(classes, r) { continue next } } return fmt.Errorf(invalidPasswordError) } return nil } //Region private methods ends