├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── demo-staging ├── backend.tf ├── clearwaiters.sh ├── copyBootstrapArtifacts.sh ├── getDomainPassword.sh ├── main.tf ├── outputs.tf ├── policy.json └── prepareProject.sh ├── docs ├── ClickToDeploy.md ├── kms.md └── runtime-config.md ├── modules ├── SQLServerWithStackdriver │ ├── main.tf │ ├── vars.tf │ └── versions.tf ├── network │ ├── main.tf │ ├── outputs.tf │ └── vars.tf ├── windowsDCWithStackdriver │ ├── main.tf │ ├── outputs.tf │ ├── vars.tf │ └── versions.tf ├── windowsWithStackdriver │ ├── main.tf │ ├── vars.tf │ └── versions.tf └── wsus │ ├── main.tf │ └── vars.tf └── powershell ├── bootstrap ├── domain-member.ps1 ├── initial_alwayson_startup_script.ps1 ├── install-sql-server-principal-step-1.ps1 ├── primary-domain-controller-step-1.ps1 ├── primary-domain-controller-step-2.ps1 └── sql_install.ps1 ├── c2d ├── c2d_base.psm1 ├── gce_base.psm1 ├── initial_win_startup_script.ps1 └── sql_install.ps1 ├── comprehensive-runtime-config.ps1 └── templates └── windows-stackdriver-setup.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | ./*.tfstate 3 | *.log 4 | *.bak 5 | .VSCodeCounter/ 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Automated Terraform AlwaysOn deployment 2 | 3 | ## What is the Goal? 4 | 5 | This will deploy a Windows environment on to Google Cloud Platform (GCP). This will include Windows Servers with SQL Server installed with AlwaysOn AG. 6 | 7 | ## Time Considerations 8 | 9 | The creation of the infrastructure is just the start, which takes about 4 minutes. 10 | 11 | It then takes about 15 minutes for the domain controller to get configured. The SQL Server VMs will wait for this to happen. After the domain controller has completed the SQL Server VMs will take an approximately 10 more minutes. 12 | 13 | ## Permissions 14 | 15 | Before you start running Terraform, you need to create a service account in your project for Terraform to run as. Under IAM and Admin --> Service accounts, create a serice account and then download the key. 16 | Give the service account the permissions it needs to create infrastructure in your project. 17 | 18 | Upload the key to wherever you are running the terraform and set GOOGLE_APPLICATION_CREDENTIALS to use this key ( or follow adding credntials in https://www.terraform.io/docs/providers/google/getting_started.html##) 19 | 20 | 21 | ## Dependencies 22 | 23 | On a machine with `terraform` (at least 0.13) and `git` (the Google Cloud Shell can be leveraged as well): 24 | 25 | ```sh 26 | gcloud source repos clone terraform-sqlserver-alwayson --project=cloud-ce-shared-code 27 | ``` 28 | 29 | Or: 30 | 31 | ```sh 32 | git clone https://github.com/GoogleCloudPlatform/terraform-sqlserver-alwayson.git 33 | ``` 34 | 35 | Let's get into that directory: 36 | 37 | ```sh 38 | cd terraform-sqlserver-alwayson/demo-staging/ 39 | ``` 40 | 41 | Here we'll update the deployment variables in `prepareProject.sh`. Edit everything within the single quotes in `prepareProject.sh` (Note: `projectNumber` doesn't need single quotes, just the number itself): 42 | 43 | ``` 44 | region='{your-region-here}' 45 | zone='{your-zone-here}' 46 | 47 | project='{your-project-id}' 48 | projectNumber={your-project-number} 49 | 50 | #differentiate this deployment from others. Use lowercase alphanumerics between 6 and 30 characters. 51 | prefix='{desired-domain-name-and-unique-seed-for-bucket-name}' 52 | 53 | #user you will be running as 54 | user='{user-you-will-run-as}' 55 | ``` 56 | 57 | :bangbang: For deployment troubleshooting, try entering in another unique `prefix`. 58 | 59 | Now we'll update the terraform project in the environment folder containing `main.tf` and `backend.tf`, by running: 60 | 61 | ```sh 62 | ./prepareProject.sh 63 | ``` 64 | 65 | Here's what's happening: 66 | 67 | * In backend.tf 68 | * bucket = "{common-backend-bucket}": change this to the bucket in your project where you will store the state 69 | * project = "{cloud-project-id}" : change this to the id of your project 70 | * In main.tf of the environment (also done by prepareProject.sh) 71 | * project = "{cloud-project-id}" 72 | * region = "{cloud-project-region}" 73 | * primaryzone = "{cloud-project-zone}" 74 | * gcs-prefix = "gs://{common-backend-bucket}" 75 | * keyring = "{deployment-name}-deployment-ring" 76 | * kms-key = "{deployment-name}-deployment-key" 77 | * domain = "{deployment-name}.com" 78 | * dc-netbios-name = "{deployment-name}" 79 | * runtime-config = "{deployment-name}-runtime-config" 80 | * Update the gcs-prefix (done in prepareProject.sh) 81 | * In GCP 82 | * Enable APIs 83 | * KMS - gcloud services enable cloudkms.googleapis.com 84 | * Runtime configurator - gcloud services enable runtimeconfig.googleapis.com 85 | * cloud resource manager 86 | * compute manager 87 | * iam 88 | * Make a bucket for: 89 | * state file 90 | * passwords 91 | * powershell scripts 92 | * copy up the required bootstrap scripts 93 | * create a new admin service account 94 | * bind the logged on user to that service account (can ran as this service account) 95 | * make the service account a project editor 96 | * create key ring 97 | * create a key 98 | * give the new admin user and the project service account rights to encrypt/decrypt with the kms key 99 | ```bash 100 | 101 | gcloud services enable cloudkms.googleapis.com 102 | gcloud services enable runtimeconfig.googleapis.com 103 | gcloud services enable cloudresourcemanager.googleapis.com 104 | gcloud services enable compute.googleapis.com 105 | gcloud services enable iam.googleapis.com 106 | 107 | #create the bucket 108 | gsutil mb -p $project gs://$bucketName 109 | gsutil -m cp -r ../powershell/bootstrap gs://$bucketName/powershell/bootstrap/ 110 | 111 | gcloud kms keyrings create acme-deployment-ring --location=us-central1 112 | gcloud kms keys create acme-deployment-key --location=us-central1 --keyring=myring --purpose=encryption 113 | 114 | ``` 115 | 116 | ## Terraforming the Environment 117 | 118 | Next we'll run: 119 | 120 | ```sh 121 | terraform init 122 | ``` 123 | Followed by 124 | 125 | ```sh 126 | terraform apply 127 | ``` 128 | You might encounter some warnings about interpolation-only expressions, due to changes between TF versions but they can safely be ignored. as of 0.13.2 129 | 130 | ## Windows Background 131 | NetBIOS is a legacy network application used by windows for active directory. It limits the names of machines to 15 characters. For this reason we must observe this limit on our computer names for our deployment to succeed. 132 | 133 | ### Naming Convention 134 | We are limited as described above. 135 | ${var.deployment-name} - a unique 8 character deployment name 136 | ${var.function} - 3 characters decribing the purpose of the instance 137 | ${var.instancenumber} - two digits 138 | computername = "${var.deployment-name}-${var.function}-${var.instancenumber}" 139 | 140 | ## For Debugging purposes: 141 | domain admin: usr: {full domain name}\Administrator pw: 142 | * Domain Controller Password: 143 | * SQL 1 Password: 144 | * SQL 2 Password: 145 | * SQL 3 Password: 146 | 147 | ## Project Layout 148 | There are folders for environment-specific content such as sandbox, clickToDeploy and acme-staging. Modules, used by the deployment scripts, can be found in the ./modules directory. The contents of the docs directory is for documentary purposes, even if it is code. The 2 shell scripts in the environment folders are: 149 | * clearwaiters.sh - if you are redeploying only the sql servers (you havent destroyed the whole environment including the runtime-config) this script will delete the waiters. 150 | * copyBootstrapArticles.sh - will copy essential scripts from ../powershell/bootstrap/ to {your deployment bucket (gcs-prefix in main.tf)}/powershell/bootstrap/ 151 | 152 | ## Runtime-Config nuances 153 | Runtime-config has limited support in terraform. In deployment manager one can create the config and variables. In terraform, you can only create the runtime config, variable and waiters must be created in powershell scipts or using command line or rest API. 154 | 155 | The following deletes a waiter, which you might need to do if you redeploy 156 | 157 | ``` bash 158 | gcloud beta runtime-config configs waiters delete clicktodeploy-dev-sql-p-01_waiter --config-name=acme-runtime-config 159 | ``` 160 | 161 | ## TO connect to the instances we need firewall rules allowing access 162 | The network module has a defult firewall resource that allows access for 3389 and 8080 to machines tagged we, pdc pr sql. If you are testing in a google project, your rules will be deleted by gce enforcer every 15 minutes and you will need to recreate your rule. 163 | 164 | 1. Go to www.whatismyip.com and find your external ip address 165 | 2. Ensure your ip address with /32 (only that ip address) is in the source range 166 | 3. Ensure your the target tags list contains the tag of the machine you are trying to get to. 167 | 168 | ``` bash 169 | terraform apply --target=module.create-network.google_compute_firewall.default 170 | ``` 171 | 172 | ## ClickToDeploy 173 | ``` Powershell 174 | 175 | $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 176 | 177 | $Script:c2d_scripts_bucket = 'c2d-windows/scripts' 178 | $Script:install_path="C:\C2D" # Folder for downloads 179 | $script:show_msgs = $false 180 | $script:write_to_serial = $false 181 | 182 | # Instance specific variables 183 | $script_name = 'sql_install.ps1' 184 | $script_subpath = 'sqlserver' 185 | $task_name = "SQLInstall" 186 | 187 | # Download the scripts 188 | # Base Script 189 | $base_script_path = "$Script:c2d_scripts_bucket/c2d_base.psm1" 190 | $base_script = "$Script:install_path\c2d_base.psm1" 191 | 192 | # Run Script 193 | $run_script = "$Script:install_path\$script_name" 194 | $run_script_path = "$Script:c2d_scripts_bucket/$script_subpath/$script_name" 195 | ``` 196 | 197 | So... 198 | ClickToDeploy depends upon (preinstalled on all windows instances): 199 | 1. C:\Program Files\Google\Compute Engine\sysprep\gce_base.psm1 200 | 201 | We are downloading: 202 | 1. From "gs://c2d-windows/scripts/c2d_base.psm1" 203 | 2. To "C:\C2D\c2d_base.psm1" 204 | 3. From: "gs://c2d-windows/scripts/sqlserver/sql_install.ps1" 205 | 4. To: "C:\C2D\sql_install.ps1" 206 | 207 | These are provided for reference purposes in powershell/c2d 208 | 209 | ### gce_base.ps1 - This provides a library of functions for interfacing with GCE (pre-installed) 210 | * Get-Metadata 211 | * Generate-Random_Password 212 | * Write-Serial-Port 213 | * Write-Log 214 | 215 | ### c2d_base.ps1 - Library of c2d flow control libraries 216 | * Write-Logger - Write log messages to instance log 217 | * Write-ToReg - Write to registry 218 | * Runtime config functions for creating configs, variables and waiters 219 | * Functions for creating and deleting scheduled tasks 220 | 221 | ### sql_install.ps1 - Everything required to configure alwayson 222 | * Create a Windows Server Failover Cluster (WSFC) 223 | * Create an availability group 224 | * Create a database 225 | * Backup and restore a database from powershell 226 | * Create shared folders 227 | * Join a domain 228 | * Setup an entire cluster 229 | 230 | sql_install.ps1 gets called without any arguments from a scheduled task. It does the following: 231 | * SetScriptVar - Setup all the variables that will be consumed later in the setup 232 | * Reads the service account from c2d-property-sa-account 233 | * Reads domain name (Fully qualified) from c2d-property-domain-dns-name 234 | * Gets the Netbios domain by splitting domain on '.' 235 | * Reads sa password from c2d-property-sa-password 236 | * Reads list of nodes in cluster from sql-nodes into all_nodes 237 | * sets static ip addresses to 10.x.1.4 238 | * sets listener ip addresses to 10.x.1.5 239 | * it is assume the gateway and DC will always be 10.0.0.100 240 | * keep list of remote nodes (nodes this isnt running on) in remote_nodes 241 | * SetIP 242 | * Set IP addresses in Script:static_ip array 243 | * Set gateway to 10.0.0.100 in $Script:static_listner_ip 244 | * Add firewall rules for SQL server (1433) and AlwaysOn (5022) at windows level 245 | 246 | On all sql instances: 247 | ``` Powershell 248 | Install-WindowsFeature RSAT-AD-PowerShell 249 | Install-WindowsFeature Failover-Clustering -IncludeManagementTools 250 | ``` 251 | 252 | ``` Powershell 253 | $Script:static_ip=@("10.10.0.3","10.10.0.4","10.10.0.5") 254 | $Script:cluster_name="cluster-dbclus" 255 | $Script:all_nodes_fqdn=@('c2d-sql-01.corp.acme.com','c2d-sql-02.corp.acme.com','c2d-sql-03.corp.acme.com') 256 | New-Cluster -Name $Script:cluster_name -Node $Script:all_nodes_fqdn -NoStorage -StaticAddress $Script:static_ip 257 | 258 | ``` 259 | 260 | #The following is how you add a cluster in powershell. This must be run as domain admin 261 | 262 | ```Powershell 263 | $Script:cluster_name='cluster-dbclust' 264 | #$Script:all_nodes_fqdn ="c2d-sql-01.acme.com,c2d-sql-02.acme.com,c2d-sql-03.acme.com" 265 | $Script:all_nodes_fqdn =@("c2d-sql-01.acme.com","c2d-sql-02.acme.com","c2d-sql-03.acme.com") 266 | $Script:static_ip= @("10.1.0.4","10.2.0.4","10.3.0.4") 267 | 268 | 269 | Write-Host "Setting up cluster $Script:cluster_name for nodes $Script:all_nodes_fqdn and ips $Script:static_ip" 270 | # Create the cluster 271 | try { 272 | $result = New-Cluster -Name $Script:cluster_name -Node $Script:all_nodes_fqdn ` 273 | -NoStorage -StaticAddress $Script:static_ip 274 | Write-Host "Result for setup cluster: $result" 275 | return $true 276 | } 277 | catch { 278 | Write-Host "** Failed to setup cluster: $Script:cluster_name ** " 279 | Write-Host $_.Exception.GetType().FullName 280 | Write-Host "$_.Exception.Message" 281 | return $false 282 | } 283 | 284 | 285 | #New-Cluster -Name "cluster-dbclus" -Node "c2d-sql-01.acme.com,c2d-sql-02.acme.com,c2d-sql-03.acme.com" -NoStorage -StaticAddress "10.1.0.4,10.2.0.4,10.3.0.4" 286 | New-Cluster -Name "cluster-dbclus" -Node @("c2d-sql-01","c2d-sql-02","c2d-sql-03") -NoStorage -StaticAddress @("10.1.0.4","10.2.0.4","10.3.0.4") 287 | 288 | ``` 289 | 290 | 291 | # Known issues 292 | * Once complete, sometimes the two replicas are not synchonized. I think this is dues to the faiure of the script executed in sql_install.ps1._DBPermission. THis is currently taking nodes as an array as a parameter but it needs to rather loop through because SUSER_ID() does not take an array as a parameter. Not a big issue though because this is just a demo db. 293 | * removing and radding the db on nodes 2 and 3 succeeds. 294 | * Once the deployment is complete, the scopes of the machines can be reset and also the access to the kms key shuld be adjusted to reflect the desired administrative priorities. 295 | * TODO: Hardcoded domain ip in sql_install.ps1 10.0.0.100 replace with fetch from metadata 296 | * TODO: the getMetaData functions and Rutime-Config functions are repeated in the gce_base.psm1, c2d_base.psm1 and also in some of the ps1 scripts. In general, if we are importing a library that contains a function, it should be used in that function rather than re-implemented locally. Refactor this code to ensure optimal definition and implimentation of common functions. 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /demo-staging/backend.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | terraform { 17 | backend "gcs" { 18 | bucket = "{common-backend-bucket}" 19 | prefix = "/states/terraform.tfstate" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo-staging/clearwaiters.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | gcloud beta runtime-config configs waiters delete c2d-sql-01_waiter --config-name={deployment-name}-runtime-config 18 | gcloud beta runtime-config configs waiters delete c2d-sql-02_waiter --config-name={deployment-name}-runtime-config 19 | gcloud beta runtime-config configs waiters delete c2d-sql-03_waiter --config-name={deployment-name}-runtime-config 20 | -------------------------------------------------------------------------------- /demo-staging/copyBootstrapArtifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | gsutil cp ../powershell/bootstrap/*.ps1 gs://{common-backend-bucket}/powershell/bootstrap/ 19 | 20 | -------------------------------------------------------------------------------- /demo-staging/getDomainPassword.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | gsutil cp gs://{common-backend-bucket}/output/domain-admin-password.bin . 17 | gcloud kms decrypt --key {deployment-name}-deployment-key --location {cloud-project-region} --keyring {deployment-name}-deployment-ring --ciphertext-file domain-admin-password.bin --plaintext-file domain-admin-password.txt 18 | cat domain-admin-password.txt 19 | rm domain-admin-password.txt 20 | -------------------------------------------------------------------------------- /demo-staging/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | data "google_project" "project" {} 18 | // Configure the Google Cloud provider 19 | provider "google" { 20 | project = "{cloud-project-id}" 21 | region = "{cloud-project-region}" 22 | } 23 | 24 | locals { 25 | primaryzone = "{cloud-project-zone}" 26 | region = "{cloud-project-region}" 27 | hazone = "{cloud-project-hazone}" 28 | drregion = "{cloud-project-drregion}" 29 | drzone = "{cloud-project-drzone}" 30 | deployment-name = "{deployment-name}" 31 | environment = "dev" 32 | osimagelinux = "projects/eip-images/global/images/rhel-7-drawfork-v20180327" 33 | osimageWindows = "windows-server-2016-dc-v20181009" 34 | osimageSQL = "projects/windows-sql-cloud/global/images/sql-2017-enterprise-windows-2016-dc-v20181009" 35 | gcs-prefix = "gs://{common-backend-bucket}" 36 | keyring = "{deployment-name}-deployment-ring" 37 | kms-key = "{deployment-name}-deployment-key" 38 | primary-cidr = "10.0.0.0/16" 39 | second-cidr = "10.1.0.0/16" 40 | second-cidr-alwayson = "10.1.0.5/32" 41 | second-cidr-wsfc = "10.1.0.4/32" 42 | third-cidr = "10.2.0.0/16" 43 | third-cidr-alwayson = "10.2.0.5/32" 44 | third-cidr-wsfc = "10.2.0.4/32" 45 | fourth-cidr = "10.3.0.0/16" 46 | fourth-cidr-alwayson = "10.3.0.5/32" 47 | fourth-cidr-wsfc = "10.3.0.4/32" 48 | domain = "{windows-domain}.com" 49 | dc-netbios-name = "{windows-domain}" 50 | runtime-config = "{deployment-name}-runtime-config" 51 | all_nodes="{deployment-name}-sql-01|{deployment-name}-sql-02|{deployment-name}-sql-03" 52 | } 53 | 54 | module "create-network"{ 55 | source = "../modules/network" 56 | network-name = "${local.deployment-name}-${local.environment}-net" 57 | primary-cidr = "${local.primary-cidr}" 58 | second-cidr = "${local.second-cidr}" 59 | third-cidr = "${local.third-cidr}" 60 | fourth-cidr = "${local.fourth-cidr}" 61 | primary-region = "${local.region}" 62 | dr-region = "${local.drregion}" 63 | deployment-name = "${local.deployment-name}" 64 | } 65 | 66 | //windows domain controller 67 | module "windows-domain-controller" { 68 | source = "../modules/windowsDCWithStackdriver" 69 | subnet-name = "${module.create-network.subnet-name}" 70 | secondary-subnet-name = "${module.create-network.subnet-name}" 71 | instancerole = "p" 72 | instancenumber = "01" 73 | function = "pdc" 74 | region = "${local.region}" 75 | keyring = "${local.keyring}" 76 | kms-key = "${local.kms-key}" 77 | kms-region ="${local.region}" 78 | environment = "${local.environment}" 79 | regionandzone = "${local.primaryzone}" 80 | osimage = "${local.osimageWindows}" 81 | gcs-prefix = "${local.gcs-prefix}" 82 | deployment-name = "${local.deployment-name}" 83 | domain-name = "${local.domain}" 84 | netbios-name = "${local.dc-netbios-name}" 85 | runtime-config = "${local.runtime-config}" 86 | wait-on = "" 87 | status-variable-path = "ad" 88 | network-tag = ["pdc"] 89 | network-ip = "10.0.0.100" 90 | } 91 | 92 | module "sql-server-alwayson-primary" { 93 | source = "../modules/SQLServerWithStackdriver" 94 | subnet-name = "${module.create-network.second-subnet-name}" 95 | alwayson-vip = "${local.second-cidr-alwayson}" 96 | wsfc-vip = "${local.second-cidr-wsfc}" 97 | instancerole = "p" 98 | instancenumber = "01" 99 | function = "sql" 100 | region = "${local.region}" 101 | keyring = "${local.keyring}" 102 | kms-key = "${local.kms-key}" 103 | kms-region="${local.region}" 104 | environment = "${local.environment}" 105 | regionandzone = "${local.primaryzone}" 106 | osimage = "${local.osimageSQL}" 107 | gcs-prefix = "${local.gcs-prefix}" 108 | deployment-name = "${local.deployment-name}" 109 | domain-name = "${local.domain}" 110 | netbios-name = "${local.dc-netbios-name}" 111 | runtime-config = "${local.runtime-config}" 112 | wait-on = "bootstrap/${local.deployment-name}/ad/success" 113 | domain-controller-address = "${module.windows-domain-controller.dc-address}" 114 | post-join-script-url = "${local.gcs-prefix}/powershell/bootstrap/install-sql-server-principal-step-1.ps1" 115 | status-variable-path = "mssql" 116 | network-tag = ["sql", "internal"] 117 | sql_nodes="${local.deployment-name}-sql-01|${local.deployment-name}-sql-02|${local.deployment-name}-sql-03" 118 | 119 | } 120 | 121 | module "sql-server-alwayson-secondary" { 122 | source = "../modules/SQLServerWithStackdriver" 123 | subnet-name = "${module.create-network.third-subnet-name}" 124 | instancerole = "s" 125 | instancenumber = "02" 126 | function = "sql" 127 | region = "${local.region}" 128 | keyring = "${local.keyring}" 129 | kms-key = "${local.kms-key}" 130 | kms-region="${local.region}" 131 | environment = "${local.environment}" 132 | regionandzone = "${local.hazone}" 133 | osimage = "${local.osimageSQL}" 134 | gcs-prefix = "${local.gcs-prefix}" 135 | deployment-name = "${local.deployment-name}" 136 | domain-name = "${local.domain}" 137 | netbios-name = "${local.dc-netbios-name}" 138 | runtime-config = "${local.runtime-config}" 139 | wait-on = "bootstrap/${local.deployment-name}/ad/success" 140 | domain-controller-address = "${module.windows-domain-controller.dc-address}" 141 | post-join-script-url = "${local.gcs-prefix}/powershell/bootstrap/install-sql-server-principal-step-1.ps1" 142 | status-variable-path = "mssql" 143 | network-tag = ["sql", "internal"] 144 | sql_nodes="${local.deployment-name}-sql-01|${local.deployment-name}-sql-02|${local.deployment-name}-sql-03" 145 | alwayson-vip = "${local.third-cidr-alwayson}" 146 | wsfc-vip = "${local.third-cidr-wsfc}" 147 | } 148 | 149 | module "sql-server-alwayson-secondary-2" { 150 | source = "../modules/SQLServerWithStackdriver" 151 | subnet-name = "${module.create-network.fourth-subnet-name}" 152 | instancerole = "s" 153 | instancenumber = "03" 154 | function = "sql" 155 | region = "${local.drregion}" 156 | keyring = "${local.keyring}" 157 | kms-key = "${local.kms-key}" 158 | kms-region="${local.region}" 159 | environment = "${local.environment}" 160 | regionandzone = "${local.drzone}" 161 | osimage = "${local.osimageSQL}" 162 | gcs-prefix = "${local.gcs-prefix}" 163 | deployment-name = "${local.deployment-name}" 164 | domain-name = "${local.domain}" 165 | netbios-name = "${local.dc-netbios-name}" 166 | runtime-config = "${local.runtime-config}" 167 | wait-on = "bootstrap/${local.deployment-name}/ad/success" 168 | domain-controller-address = "${module.windows-domain-controller.dc-address}" 169 | post-join-script-url = "${local.gcs-prefix}/powershell/bootstrap/install-sql-server-principal-step-1.ps1" 170 | status-variable-path = "mssql" 171 | network-tag = ["sql", "internal"] 172 | sql_nodes="${local.deployment-name}-sql-01|${local.deployment-name}-sql-02|${local.deployment-name}-sql-03" 173 | alwayson-vip = "${local.fourth-cidr-alwayson}" 174 | wsfc-vip = "${local.fourth-cidr-wsfc}" 175 | } 176 | -------------------------------------------------------------------------------- /demo-staging/outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | output domain-controller-address { 17 | value = "${module.windows-domain-controller.dc-address}" 18 | } 19 | -------------------------------------------------------------------------------- /demo-staging/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [{ 3 | "role": "roles/cloudkms.cryptoKeyDecrypter", 4 | "members": ["serviceAccount:{SvcAccount}", "user:{Usr}"] 5 | }, { 6 | "role": "roles/cloudkms.cryptoKeyEncrypter", 7 | "members": ["serviceAccount:{SvcAccount}", "user:{Usr}"] 8 | }] 9 | } 10 | -------------------------------------------------------------------------------- /demo-staging/prepareProject.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | #Enter your project details for setting up the project dependencies 19 | region='{your-region-here}' 20 | zone=$region"-a" 21 | hazone=$region"-b" 22 | drregion='{your-dr-region}' #eg. us-east1 (b,c and d are valid) 23 | drzone=$drregion"-b" 24 | 25 | #Generate a uniqueu prefix for the bucket 26 | uniq=$(head /dev/urandom | tr -dc a-z0-9 | head -c 13 ; echo '') 27 | 28 | project='{your-project-id}' 29 | projectNumber={your-project-number} 30 | 31 | #differentiate this deployment from others. Use lowercase alphanumerics up to 8 characters. 32 | prefix='{desired-unique-prefix-for-resources}' 33 | 34 | #domain name (this will have .com added to make it fully qualified) 35 | domainName='{domain}' 36 | 37 | #user you will be running as (a fully google or gmail email address) 38 | user='{user-you-will-run-as}' 39 | 40 | ####################################################################################### 41 | ### For the purposes of this demo script, you dont need to fill in anything past here 42 | ####################################################################################### 43 | 44 | #bucket where your terraform state file, passwords and outputs will be stored 45 | bucketName=$uniq'-deployment-staging' 46 | 47 | kmsKeyRing=$prefix"-deployment-ring" 48 | kmsKey=$prefix"-deployment-key" 49 | 50 | echo $prefix 51 | echo $bucketName 52 | echo $kmsKeyRing 53 | echo $kmsKey 54 | 55 | # The files we have to substitute in are: 56 | # backend.tf clearwaiters.sh copyBootstrapArtifacts.sh getDomainPassword.sh main.tf 57 | sed -i "s/{common-backend-bucket}/$bucketName/g;s/{windows-domain}/$domainName/g;s/{cloud-project-id}/$project/g;s/{cloud-project-region}/$region/g;s/{cloud-project-zone}/$zone/g;s/{cloud-project-hazone}/$hazone/g;s/{cloud-project-drregion}/$drregion/g;s/{cloud-project-drzone}/$drzone/g;s/{deployment-name}/$prefix/g" backend.tf main.tf clearwaiters.sh copyBootstrapArtifacts.sh getDomainPassword.sh 58 | 59 | ######################################### 60 | #enable the services that we depend upon 61 | ########################################## 62 | for API in compute cloudkms deploymentmanager runtimeconfig cloudresourcemanager iam storage-api storage-component 63 | do 64 | gcloud services enable "$API.googleapis.com" --project $project 65 | done 66 | 67 | #create the bucket 68 | gsutil mb -p $project gs://$bucketName 69 | gsutil -m cp -r ../powershell/bootstrap/* gs://$bucketName/powershell/bootstrap/ 70 | 71 | DefaultServiceAccount="$projectNumber-compute@developer.gserviceaccount.com" 72 | AdminServiceAccountName="admin-$prefix" 73 | echo AdminServiceAccountName 74 | 75 | AdminServiceAccount="$AdminServiceAccountName@$project.iam.gserviceaccount.com" 76 | echo $AdminServiceAccount 77 | 78 | gcloud iam service-accounts create $AdminServiceAccountName --display-name "Admin service account for bootstrapping domain-joined servers with elevated permissions" --project $project 79 | gcloud iam service-accounts add-iam-policy-binding $AdminServiceAccount --member "user:$user" --role "roles/iam.serviceAccountUser" --project $project 80 | gcloud projects add-iam-policy-binding $project --member "serviceAccount:$AdminServiceAccount" --role "roles/editor" 81 | 82 | ServiceAccount=$AdminServiceAccount 83 | echo "Service Account: [$ServiceAccount]" 84 | 85 | 86 | gcloud kms keyrings create $kmsKeyRing --project $project --location $region 87 | gcloud kms keys create $kmsKey --project $project --purpose=encryption --keyring $kmsKeyRing --location $region 88 | 89 | sed "s/{Usr}/$user/g;s/{SvcAccount}/$ServiceAccount/g" policy.json | tee policy.out 90 | echo $policy 91 | 92 | 93 | gcloud kms keys set-iam-policy $kmsKey policy.out --project $project --location=$region --keyring=$kmsKeyRing 94 | rm policy.out 95 | 96 | 97 | sed "s/{Usr}/$user/g;s/{SvcAccount}/$DefaultServiceAccount/g" policy.json | tee policy.out 98 | gcloud kms keys set-iam-policy $kmsKey policy.out --project $project --location=$region --keyring=$kmsKeyRing 99 | rm policy.out 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/ClickToDeploy.md: -------------------------------------------------------------------------------- 1 | # ClickToDeploy 2 | 3 | The clicktodeploy pattern is a one click deployment pattern used to deploy environments from the google marketplace. It requires a minimal number of inputs to define the environment, and with those it uses deployment manager, python (and powershell on windows) to automate the end-to-end deployment. 4 | 5 | For the original click to deploy search the google cloud marketplace for SQL Server 2016 AlwaysOn Failover cluster instance 6 | 7 | It requires 3 files: 8 | * windows-startup-script-ps1 9 | * defaulted on install 10 | * downloads the following 2 scripts to c:\C2D 11 | * runs c:\c2D\sql_install.ps1 as a schedulted task 12 | * sql_install.ps1 - downloaded from gs://c2d-windows/scripts/sqlserver 13 | * parameters come from metadata of the instance 14 | * c2d-property-sa-account : domain admin account 15 | * c2d-property-sa-password : domain admin password 16 | * c2d-property-domain-dns-name : Fully qualified domain name 17 | * sql-nodes : pipe delimited list of sql server nodes 18 | * states are stored in the following keys - create these keys yourself to skip steps 19 | * $sql_on_domain_reg = "HKLM:\SOFTWARE\Google\SQLOnDomain" 20 | * $sql_configured_reg = "HKLM:\SOFTWARE\Google\SQLServerConfigured" 21 | * $sql_server_task = "HKLM:\SOFTWARE\Google\SQLServerTask" 22 | * $shares_already_created_reg = "HKLM:\SOFTWARE\Google\SharesCreated" 23 | * WSFC cluster is setup on node 1 24 | * AlwaysOn is enabled 25 | * VIP is configured 26 | * c2d_base.psm1 - downloaded from gs://c2d-windows/scripts/ 27 | * gce_base.psm1 - pre-installed to C:\Program Files\Google\Compute Engine\sysprep 28 | 29 | The following is the code that defines the downloading process. 30 | 31 | ``` Powershell 32 | 33 | $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 34 | 35 | $Script:c2d_scripts_bucket = 'c2d-windows/scripts' 36 | $Script:install_path="C:\C2D" # Folder for downloads 37 | $script:show_msgs = $false 38 | $script:write_to_serial = $false 39 | 40 | # Instance specific variables 41 | $script_name = 'sql_install.ps1' 42 | $script_subpath = 'sqlserver' 43 | $task_name = "SQLInstall" 44 | 45 | # Download the scripts 46 | # Base Script 47 | $base_script_path = "$Script:c2d_scripts_bucket/c2d_base.psm1" 48 | $base_script = "$Script:install_path\c2d_base.psm1" 49 | 50 | # Run Script 51 | $run_script = "$Script:install_path\$script_name" 52 | $run_script_path = "$Script:c2d_scripts_bucket/$script_subpath/$script_name" 53 | ``` 54 | 55 | So... 56 | ClickToDeploy depends upon (preinstalled on all windows instances): 57 | 1. C:\Program Files\Google\Compute Engine\sysprep\gce_base.psm1 58 | 59 | We are downloading: 60 | 1. From "gs://c2d-windows/scripts/c2d_base.psm1" 61 | 2. To "C:\C2D\c2d_base.psm1" 62 | 3. From: "gs://c2d-windows/scripts/sqlserver/sql_install.ps1" 63 | 4. To: "C:\C2D\sql_install.ps1" 64 | 65 | These are provided for reference purposes in powershell/c2d 66 | 67 | ### gce_base.ps1 - This provides a library of functions for interfacing with GCE (pre-installed) 68 | * Get-Metadata 69 | * Generate-Random_Password 70 | * Write-Serial-Port 71 | * Write-Log 72 | 73 | ### c2d_base.ps1 - Library of c2d flow control libraries 74 | * Write-Logger - Write log messages to instance log 75 | * Write-ToReg - Write to registry 76 | * Runtime config functions for creating configs, variables and waiters 77 | * Functions for creating and deleting scheduled tasks 78 | 79 | ### sql_install.ps1 - Everything required to configure alwayson 80 | * Create a Windows Server Failover Cluster (WSFC) 81 | * Create an availability group 82 | * Create a database 83 | * Backup and restore a database from powershell 84 | * Create shared folders 85 | * Join a domain 86 | * Setup an entire cluster 87 | 88 | sql_install.ps1 gets called without any arguments from a scheduled task. It does the following: 89 | * SetScriptVar - Setup all the variables that will be consumed later in the setup 90 | * Reads the service account from c2d-property-sa-account 91 | * Reads domain name (Fully qualified) from c2d-property-domain-dns-name 92 | * Gets the Netbios domain by splitting domain on '.' 93 | * Reads sa password from c2d-property-sa-password 94 | * Reads list of nodes in cluster from sql-nodes into all_nodes 95 | * sets static ip addresses to 10.x.1.4 96 | * sets listener ip addresses to 10.x.1.5 97 | * it is assume the gateway and DC will always be 10.0.0.100 98 | * keep list of remote nodes (nodes this isnt running on) in remote_nodes 99 | * SetIP 100 | * Set IP addresses in Script:static_ip array 101 | * Set gateway to 10.0.0.100 in $Script:static_listner_ip 102 | * Add firewall rules for SQL server (1433) and AlwaysOn (5022) at windows level 103 | 104 | On all sql instances: 105 | ``` Powershell 106 | Install-WindowsFeature RSAT-AD-PowerShell 107 | Install-WindowsFeature Failover-Clustering -IncludeManagementTools 108 | ``` 109 | 110 | ``` Powershell 111 | $Script:static_ip=@("10.10.0.3","10.10.0.4","10.10.0.5") 112 | $Script:cluster_name="cluster-dbclus" 113 | $Script:all_nodes_fqdn=@('c2d-sql-01.corp.acme.com','c2d-sql-02.corp.acme.com','c2d-sql-03.corp.acme.com') 114 | New-Cluster -Name $Script:cluster_name -Node $Script:all_nodes_fqdn -NoStorage -StaticAddress $Script:static_ip 115 | 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/kms.md: -------------------------------------------------------------------------------- 1 | # Key Management Service 2 | 3 | We create a random password for local admin and safemode admin in primary-domain-controller-step-1.ps1. These are stored in SecureStrings. 4 | 5 | This project is depedent upon the creation of a kms ring and key that you will reference from the metadata. 6 | 7 | ```powershell 8 | 9 | $KmsKey = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/kms-key 10 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 11 | $Region = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/region 12 | $Keyring = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring 13 | 14 | 15 | $SafeModeAdminPassword = New-RandomPassword 16 | $LocalAdminPassword = New-RandomPassword 17 | 18 | Set-LocalUser Administrator -Password $LocalAdminPassword 19 | Enable-LocalUser Administrator 20 | 21 | Write-Host "Saving encrypted credentials in GCS..." 22 | 23 | $TempFile = New-TemporaryFile 24 | 25 | Unwrap-SecureString $LocalAdminPassword | gcloud kms encrypt --key $KmsKey --plaintext-file - --ciphertext-file $TempFile.FullName --location $Region --keyring $Keyring 26 | gsutil cp $TempFile.FullName "$GcsPrefix/output/domain-admin-password.bin" 27 | 28 | Unwrap-SecureString $SafeModeAdminPassword | gcloud kms encrypt --key $KmsKey --plaintext-file - --ciphertext-file $TempFile.FullName --location $Region --keyring $Keyring 29 | gsutil cp $TempFile.FullName "$GcsPrefix/output/dsrm-admin-password.bin" 30 | 31 | Remove-Item $TempFile.FullName -Force 32 | 33 | ``` 34 | Now decrypt when you need it 35 | ```powershell 36 | $TempFile = New-TemporaryFile 37 | 38 | # invoke-command sees gsutil output as an error so redirect stderr to stdout and stringify to suppress 39 | gsutil cp $GcsPrefix/output/domain-admin-password.bin $TempFile.FullName 2>&1 | %{ "$_" } 40 | 41 | $DomainAdminPassword = $(gcloud kms decrypt --key $KmsKey --location $Region --keyring $Keyring --ciphertext-file $TempFile.FullName --plaintext-file - | ConvertTo-SecureString -AsPlainText -Force) 42 | 43 | Remove-Item $TempFile.FullName 44 | ``` 45 | 46 | KMS is used to encrypt the password to a temporary file which is copied to cloud storage. This process is dependent upon the existence of a keyring and key in the specified region. 47 | 48 | 49 | ```bash 50 | #create a keyring 51 | gcloud kms keyrings create myring --location=us-central1 52 | 53 | #create an encryption key 54 | gcloud kms keys create mykey --location=us-central1 --keyring=myring --purpose=encryption 55 | 56 | #list rings 57 | gcloud kms keyrings list --location=us-central1 58 | 59 | NAME 60 | projects/{project-name}/locations/us-central1/keyRings/acme-deployment-ring 61 | projects/{project-name}/locations/us-central1/keyRings/acme-ring 62 | 63 | 64 | gcloud kms keys list --location=us-central1 --keyring=acme-deployment-ring 65 | 66 | NAME PURPOSE LABELS PRIMARY_ID PRIMARY_STATE 67 | projects/{project-name}/locations/us-central1/keyRings/acme-deployment-ring/cryptoKeys/acme-deployment-key ENCRYPT_DECRYPT 1 ENABLED 68 | 69 | ``` 70 | 71 | In bash you can download the file as follows 72 | ```bash 73 | gsutil cp gs://acme-deployment/output/domain-admin-password.bin . 74 | 75 | gcloud kms decrypt --key acme-deployment-key --location us-central1 --keyring acme-deployment-ring --ciphertext-file 76 | domain-admin-password.bin --plaintext-file domain-admin-password.txt 77 | 78 | ``` 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/runtime-config.md: -------------------------------------------------------------------------------- 1 | #Runtime Config 2 | 3 | Runtime config variables are project scoped key value pairs that allow you to: 4 | * Dynamically configure services 5 | * Communicate service states 6 | * Send notification of changes to data 7 | * Share information between multiple tiers of services 8 | 9 | We create a runtime config resource in Terraform in the creation of the domain controller (in this case the variable value is "acme-runtime-config"). 10 | 11 | ```terraform 12 | resource "google_runtimeconfig_config" "ad-runtime-config" { 13 | name = "${var.runtime-config}" 14 | description = "Runtime configuration values for my service" 15 | } 16 | 17 | ``` 18 | 19 | This is created with the terraform apply at the time of creation of the domain controller and the will be the basis of a number of runtime config variables that will be used for the synchronisation of our deployment process. Most of it will be done from powershell which is where most of our windows configuration happens. 20 | 21 | The runtime config of a project can be found at the following full path: 22 | 23 | https://runtimeconfig.googleapis.com/v1beta1/projects/{project id}/configs/{runtime-config}. 24 | 25 | This is important because some methods reuire the full path to the config and variables while others do not. The clicktodeploy code which I am leveraging, requires that in metadata is a key value pair as follows: 26 | status-config-url:https://runtimeconfig.googleapis.com/v1beta1/projects/{project-name}/configs/acme-runtime-config 27 | 28 | 29 | # Get RuntimeConfig URL for the deployment 30 | 31 | THis is important because we cannot change code in c2d_base or gce_base as they are common public libraries. We have to provide the appropriate inputs which is the status-config-url metadata key. 32 | 33 | ### In c2d_base.ps1 (Imports gce_base.ps1) 34 | NOTE: 35 | * c2d_base.ps1 is downloaded from gs://c2d-windows/scripts to c:/c2d/ in install-sql-server-principal-step-1.ps1 36 | * gce_base.ps1 is in C:\Program Files\Google\Compute Engine\sysprep on all gce machines 37 | 38 | 39 | ```powershell 40 | $runtime_config = _FetchFromMetaData -property 'attributes/status-config-url' 41 | 42 | if ($runtime_config) { 43 | # Use second part of the config URL 44 | #run_time_base='https://runtimeconfig.googleapis.com/v1beta1' 45 | $config_name = (($runtime_config -split "$Script:run_time_base/")[1]) 46 | return $config_name 47 | } 48 | else { 49 | Write-Log 'No RunTimeConfig found URL found in metadata.' -error 50 | return $false 51 | } 52 | ``` 53 | The split results in $config_name=/projects/{project-name}/configs/acme-runtime-config 54 | 55 | We are now set up such that the sql_install.ps1 script will work. 56 | 57 | 58 | 59 | # After the domain controller installs 60 | 61 | We fetch the necessary variables from metadata and set the runtime config variable. 62 | 63 | ```powershell 64 | 65 | --flag completion of bootstrap requires beta gcloud component 66 | $projectId = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/project-id 67 | $RuntimeConfig = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/runtime-config 68 | $deploymentName = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/deployment-name 69 | $statusPath = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/status-variable-path 70 | 71 | 72 | Set-RuntimeConfigVariable -ConfigPath "projects/$projectId/configs/$RuntimeConfig" -Variable bootstrap/$deploymentName/$statusPath/success/time -Text (Get-Date -Format g) 73 | 74 | ``` 75 | 76 | This results in a key of: 77 | "projects/{project-name}/configs/acme-runtime-config/variables/bootstrap/c2d/ad/success/time" having a value of the current time. 78 | 79 | # SQL Servers install WSFC and then wait. 80 | 81 | We can list the configs 82 | ```bash 83 | gcloud beta runtime-config configs list 84 | 85 | NAME DESCRIPTION 86 | acme-runtime-config Runtime configuration values for my service 87 | ``` 88 | 89 | We can list the variables created by the process 90 | ```bash 91 | gcloud beta runtime-config configs variables list --config-name=acme-runtime-config 92 | 93 | NAME UPDATE_TIME 94 | backup/success/done 2018-12-05T23:38:55.792737384Z 95 | bootstrap/c2d/ad/success/time 2018-12-05T23:31:54.887933211Z 96 | cluster/success/done 2018-12-05T23:38:49.315840745Z 97 | initdb/success/done 2018-12-05T23:39:39.210801908Z 98 | replica/success/done 2018-12-05T23:39:56.925089787Z 99 | status/success/1768351525 2018-12-05T23:40:20.676334530Z 100 | status/success/2033934784 2018-12-05T23:40:19.905856083Z 101 | status/success/255883414 2018-12-05T23:40:40.431274139Z 102 | success/1710287108 2018-12-05T23:34:30.532767805Z 103 | success/825333265 2018-12-05T23:35:04.360200890Z 104 | success/865857694 2018-12-05T23:34:25.276051895Z 105 | ``` 106 | 107 | We can list the waiters that were created to block while waiting for the domain to come up 108 | 109 | ```bash 110 | gcloud beta runtime-config configs waiters list --config-name=acme-runtime-config 111 | NAME CREATE_TIME WAITER_STATUS MESSAGE 112 | c2d-sql-01_waiter 2018-12-05T23:23:32 SUCCESS 113 | c2d-sql-02_waiter 2018-12-05T23:22:58 SUCCESS 114 | c2d-sql-03_waiter 2018-12-05T23:23:00 SUCCESS 115 | ``` 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /modules/SQLServerWithStackdriver/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | data "google_project" "project" { 17 | } 18 | 19 | #Setup a template for the windows startup bat file. The bat files calls a powershell script so that is can get permissios to install stackdriver 20 | data "template_file" "windowsstartup" { 21 | template = file("../powershell/templates/windows-stackdriver-setup.ps1") 22 | 23 | vars = { 24 | environment = var.environment 25 | projectname = lower(data.google_project.project.name) 26 | computername = "${var.deployment-name}-${var.function}-${var.instancenumber}" 27 | } 28 | } 29 | 30 | locals { 31 | computername = "${var.deployment-name}-${var.function}-${var.instancenumber}" 32 | } 33 | 34 | resource "google_compute_disk" "datadisk" { 35 | name = "${local.computername}-pd-standard" 36 | zone = var.regionandzone 37 | type = "pd-standard" 38 | size = "200" 39 | } 40 | 41 | resource "google_compute_instance" "sqlserver" { 42 | name = local.computername 43 | machine_type = var.machinetype 44 | zone = var.regionandzone 45 | boot_disk { 46 | initialize_params { 47 | image = var.osimage 48 | size = "200" 49 | type = "pd-standard" 50 | } 51 | } 52 | 53 | network_interface { 54 | subnetwork = var.subnet-name 55 | alias_ip_range { 56 | ip_cidr_range = var.alwayson-vip 57 | } 58 | alias_ip_range { 59 | ip_cidr_range = var.wsfc-vip 60 | } 61 | access_config { 62 | // Ephemeral IP 63 | } 64 | } 65 | 66 | attached_disk { 67 | source = "${local.computername}-pd-standard" 68 | device_name = "appdata" 69 | } 70 | 71 | depends_on = [google_compute_disk.datadisk] 72 | 73 | tags = var.network-tag 74 | 75 | metadata = { 76 | environment = var.environment 77 | domain-name = var.domain-name 78 | domain-controller-address = var.domain-controller-address 79 | instancerole = var.instancerole 80 | function = var.function 81 | region = var.region 82 | keyring = var.keyring 83 | keyring-region = var.kms-region 84 | runtime-config = var.runtime-config 85 | kms-key = var.kms-key 86 | gcs-prefix = var.gcs-prefix 87 | netbios-name = var.netbios-name 88 | application = "SQLServer AlwaysOn" 89 | windows-startup-script-ps1 = data.template_file.windowsstartup.rendered 90 | role = var.instancerole 91 | wait-on = var.wait-on 92 | project-id = lower(data.google_project.project.project_id) 93 | post-join-script-url = var.post-join-script-url 94 | sql_nodes = var.sql_nodes 95 | } 96 | 97 | service_account { 98 | //scopes = ["storage-ro","monitoring-write","logging-write","trace-append"] 99 | scopes = ["cloud-platform", "https://www.googleapis.com/auth/cloudruntimeconfig", "storage-rw"] 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /modules/SQLServerWithStackdriver/vars.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | variable "alwayson-vip" { 17 | type = string 18 | description = "address where alwayson will listen" 19 | } 20 | 21 | variable "wsfc-vip" { 22 | type = string 23 | description = "address where wsfc will listen" 24 | } 25 | 26 | variable "machinetype" { 27 | type = string 28 | default = "n1-standard-4" 29 | } 30 | 31 | variable "osimage" { 32 | type = string 33 | } 34 | 35 | variable "environment" { 36 | type = string 37 | } 38 | 39 | variable "instancerole" { 40 | type = string 41 | default = "p" 42 | } 43 | 44 | variable "function" { 45 | type = string 46 | default = "sql" 47 | } 48 | 49 | variable "instancenumber" { 50 | type = string 51 | default = "01" 52 | } 53 | 54 | variable "regionandzone" { 55 | type = string 56 | } 57 | 58 | variable "deployment-name" { 59 | type = string 60 | default = "" 61 | } 62 | 63 | variable "assignedsubnet" { 64 | type = string 65 | default = "default" 66 | } 67 | 68 | variable "domain-name" { 69 | type = string 70 | default = "test-domain" 71 | } 72 | 73 | variable "kms-key" { 74 | type = string 75 | default = "p@ssword" 76 | } 77 | 78 | variable "kms-region" { 79 | type = string 80 | default = "us-central1" 81 | } 82 | 83 | variable "gcs-prefix" { 84 | type = string 85 | } 86 | 87 | variable "region" { 88 | type = string 89 | } 90 | 91 | variable "subnet-name" { 92 | type = string 93 | } 94 | 95 | variable "netbios-name" { 96 | type = string 97 | } 98 | 99 | variable "runtime-config" { 100 | type = string 101 | } 102 | 103 | variable "keyring" { 104 | type = string 105 | } 106 | 107 | variable "wait-on" { 108 | type = string 109 | } 110 | 111 | variable "domain-controller-address" { 112 | type = string 113 | } 114 | 115 | variable "status-variable-path" { 116 | type = string 117 | } 118 | 119 | variable "network-tag" { 120 | type = list(string) 121 | default = [""] 122 | description = "network tags" 123 | } 124 | 125 | variable "post-join-script-url" { 126 | type = string 127 | default = "" 128 | description = "after joining to the domain" 129 | } 130 | 131 | variable "sql_nodes" { 132 | type = string 133 | default = "" 134 | description = "list of sql nodes in cluster" 135 | } 136 | 137 | -------------------------------------------------------------------------------- /modules/SQLServerWithStackdriver/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/network/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | resource "google_compute_subnetwork" "primary-subnetwork" { 17 | name = "${var.network-name}-subnet-1" 18 | ip_cidr_range = "${var.primary-cidr}" 19 | region = "${var.primary-region}" 20 | network = "${google_compute_network.custom-network.self_link}" 21 | } 22 | 23 | resource "google_compute_subnetwork" "subnetwork-2" { 24 | name = "${var.network-name}-subnet-2" 25 | ip_cidr_range = "${var.second-cidr}" 26 | region = "${var.primary-region}" 27 | network = "${google_compute_network.custom-network.self_link}" 28 | } 29 | 30 | resource "google_compute_subnetwork" "subnetwork-3" { 31 | name = "${var.network-name}-subnet-3" 32 | ip_cidr_range = "${var.third-cidr}" 33 | region = "${var.primary-region}" 34 | network = "${google_compute_network.custom-network.self_link}" 35 | } 36 | 37 | resource "google_compute_subnetwork" "subnetwork-4" { 38 | name = "${var.network-name}-subnet-4" 39 | ip_cidr_range = "${var.fourth-cidr}" 40 | region = "${var.dr-region}" 41 | network = "${google_compute_network.custom-network.self_link}" 42 | } 43 | 44 | resource "google_compute_network" "custom-network" { 45 | name = "${var.network-name}" 46 | auto_create_subnetworks = false 47 | } 48 | 49 | resource "google_compute_firewall" "default" { 50 | name = "${var.deployment-name}-allow-remote-access" 51 | network = "${google_compute_network.custom-network.self_link}" 52 | 53 | 54 | allow { 55 | protocol = "tcp" 56 | ports = ["3389", "8080"] 57 | } 58 | 59 | source_ranges = ["35.185.218.131/32"] 60 | target_tags = ["web","pdc","sql"] 61 | } 62 | 63 | resource "google_compute_firewall" "allow-internal" { 64 | name = "${var.deployment-name}-allow-internal" 65 | network = "${google_compute_network.custom-network.self_link}" 66 | 67 | allow { 68 | protocol = "all" 69 | } 70 | 71 | source_tags = ["pdc","sql"] 72 | target_tags = ["sql","pdc"] 73 | } 74 | 75 | resource "google_compute_firewall" "healthchecks" { 76 | name = "${var.deployment-name}-allow-healthcheck-access" 77 | network = "${google_compute_network.custom-network.self_link}" 78 | 79 | 80 | allow { 81 | protocol = "tcp" 82 | ports = ["1-65535"] 83 | } 84 | 85 | source_ranges = ["130.211.0.0/22","35.191.0.0/16"] 86 | target_tags = ["sql"] 87 | } 88 | 89 | resource "google_compute_firewall" "alwayson" { 90 | name = "${var.deployment-name}-allow-alwayson-access" 91 | network = "${google_compute_network.custom-network.self_link}" 92 | 93 | 94 | allow { 95 | protocol = "tcp" 96 | ports = ["5022"] 97 | } 98 | 99 | source_tags = ["sql"] 100 | target_tags = ["sql"] 101 | } 102 | -------------------------------------------------------------------------------- /modules/network/outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | output "subnet-name" { 17 | value = "${google_compute_subnetwork.primary-subnetwork.name}" 18 | #value = "test-string" 19 | } 20 | output "second-subnet-name" { 21 | value = "${google_compute_subnetwork.subnetwork-2.name}" 22 | #value = "test-string" 23 | } 24 | output "third-subnet-name" { 25 | value = "${google_compute_subnetwork.subnetwork-3.name}" 26 | #value = "test-string" 27 | } 28 | output "fourth-subnet-name" { 29 | value = "${google_compute_subnetwork.subnetwork-4.name}" 30 | #value = "test-string" 31 | } 32 | -------------------------------------------------------------------------------- /modules/network/vars.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | variable "network-name" { 17 | type = "string" 18 | default = "custom-network" 19 | } 20 | variable "primary-cidr" { 21 | type = "string" 22 | default = "10.10.1.0/16" 23 | } 24 | variable "second-cidr" { 25 | type = "string" 26 | default = "10.11.1.0/16" 27 | } 28 | variable "third-cidr" { 29 | type = "string" 30 | default = "10.12.1.0/16" 31 | } 32 | variable "fourth-cidr" { 33 | type = "string" 34 | default = "10.13.1.0/16" 35 | } 36 | variable "deployment-name" { 37 | type = "string" 38 | default = "depl" 39 | } 40 | variable "primary-region" { 41 | type = "string" 42 | default = "us-central1" 43 | } 44 | variable "dr-region" { 45 | type = "string" 46 | default = "us-east1" 47 | } 48 | -------------------------------------------------------------------------------- /modules/windowsDCWithStackdriver/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | data "google_project" "project" { 17 | } 18 | 19 | #Setup a template for the windows startup bat file. The bat files calls a powershell script so that is can get permissios to install stackdriver 20 | data "template_file" "windowsstartup" { 21 | template = file("../powershell/templates/windows-stackdriver-setup.ps1") 22 | 23 | vars = { 24 | environment = var.environment 25 | projectname = lower(data.google_project.project.name) 26 | computername = "${var.deployment-name}-${var.function}-${var.instancenumber}" 27 | } 28 | } 29 | 30 | locals { 31 | computername = "${var.deployment-name}-${var.function}-${var.instancenumber}" 32 | } 33 | 34 | resource "google_compute_disk" "datadisk" { 35 | name = "${local.computername}-pd-standard" 36 | zone = var.regionandzone 37 | type = "pd-standard" 38 | size = "200" 39 | } 40 | 41 | resource "google_runtimeconfig_config" "ad-runtime-config" { 42 | name = var.runtime-config 43 | description = "Runtime configuration values for my service" 44 | } 45 | 46 | resource "google_compute_instance" "domain-controller" { 47 | name = local.computername 48 | machine_type = var.machinetype 49 | zone = var.regionandzone 50 | 51 | boot_disk { 52 | initialize_params { 53 | image = var.osimage 54 | size = "200" 55 | type = "pd-standard" 56 | } 57 | } 58 | 59 | network_interface { 60 | subnetwork = var.subnet-name 61 | network_ip = var.network-ip 62 | access_config { 63 | } 64 | } 65 | 66 | attached_disk { 67 | source = "${local.computername}-pd-standard" 68 | device_name = "appdata" 69 | } 70 | 71 | depends_on = [google_compute_disk.datadisk] 72 | 73 | tags = var.network-tag 74 | 75 | metadata = { 76 | environment = var.environment 77 | domain-name = var.domain-name 78 | function = var.function 79 | region = var.region 80 | keyring = var.keyring 81 | runtime-config = var.runtime-config 82 | deployment-name = var.deployment-name 83 | kms-key = var.kms-key 84 | keyring-region = var.kms-region 85 | gcs-prefix = var.gcs-prefix 86 | netbios-name = var.netbios-name 87 | application = "primary domain controller" 88 | windows-startup-script-ps1 = data.template_file.windowsstartup.rendered 89 | role = var.instancerole 90 | status-variable-path = var.status-variable-path 91 | project-id = lower(data.google_project.project.project_id) 92 | } 93 | 94 | service_account { 95 | //scopes = ["storage-ro","monitoring-write","logging-write","trace-append"] 96 | scopes = ["https://www.googleapis.com/auth/cloud-platform"] 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /modules/windowsDCWithStackdriver/outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | locals { 17 | nic = google_compute_instance.domain-controller.network_interface[0] 18 | } 19 | 20 | output "dc-address" { 21 | value = local.nic["network_ip"] 22 | } 23 | 24 | -------------------------------------------------------------------------------- /modules/windowsDCWithStackdriver/vars.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | variable "machinetype" { 17 | type = string 18 | default = "n1-standard-8" 19 | } 20 | 21 | variable "osimage" { 22 | type = string 23 | } 24 | 25 | variable "environment" { 26 | type = string 27 | } 28 | 29 | variable "instancerole" { 30 | type = string 31 | default = "p" 32 | } 33 | 34 | variable "function" { 35 | type = string 36 | default = "pdc" 37 | } 38 | 39 | variable "instancenumber" { 40 | type = string 41 | default = "01" 42 | } 43 | 44 | variable "regionandzone" { 45 | type = string 46 | } 47 | 48 | variable "deployment-name" { 49 | type = string 50 | default = "" 51 | } 52 | 53 | variable "assignedsubnet" { 54 | type = string 55 | default = "default" 56 | } 57 | 58 | variable "domain-name" { 59 | type = string 60 | default = "test-domain" 61 | } 62 | 63 | variable "kms-key" { 64 | type = string 65 | default = "p@ssword" 66 | } 67 | 68 | variable "kms-region" { 69 | type = string 70 | default = "us-central1" 71 | } 72 | 73 | variable "gcs-prefix" { 74 | type = string 75 | } 76 | 77 | variable "region" { 78 | type = string 79 | } 80 | 81 | variable "subnet-name" { 82 | type = string 83 | } 84 | 85 | variable "secondary-subnet-name" { 86 | type = string 87 | } 88 | 89 | variable "netbios-name" { 90 | type = string 91 | } 92 | 93 | variable "runtime-config" { 94 | type = string 95 | } 96 | 97 | variable "keyring" { 98 | type = string 99 | } 100 | 101 | variable "wait-on" { 102 | type = string 103 | } 104 | 105 | variable "status-variable-path" { 106 | type = string 107 | } 108 | 109 | variable "network-tag" { 110 | type = list(string) 111 | default = [""] 112 | description = "network tags" 113 | } 114 | 115 | variable "network-ip" { 116 | type = string 117 | default = "" 118 | } 119 | 120 | #variable "project-id" { 121 | # type="string" 122 | #} 123 | -------------------------------------------------------------------------------- /modules/windowsDCWithStackdriver/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/windowsWithStackdriver/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | data "google_project" "project" { 17 | } 18 | 19 | #Setup a template for the windows startup bat file. The bat files calls a powershell script so that is can get permissios to install stackdriver 20 | data "template_file" "windowsstartup" { 21 | template = file("../powershell/windows-stackdriver-setup.ps1") 22 | 23 | vars = { 24 | environment = var.environment 25 | projectname = lower(data.google_project.project.name) 26 | computername = "${var.deployment-name}-${var.environment}-${var.function}-${var.instancerole}-${var.instancenumber}" 27 | } 28 | } 29 | 30 | resource "google_compute_disk" "datadisk" { 31 | name = "${var.deployment-name}-${var.environment}-${var.function}-${var.instancerole}-${var.instancenumber}-pd-standard" 32 | zone = var.regionandzone 33 | type = "pd-standard" 34 | size = "200" 35 | } 36 | 37 | resource "google_compute_instance" "windows" { 38 | name = "${var.deployment-name}-${var.environment}-${var.function}-${var.instancerole}-${var.instancenumber}" 39 | machine_type = var.machinetype 40 | zone = var.regionandzone 41 | boot_disk { 42 | initialize_params { 43 | image = var.osimage 44 | size = "200" 45 | type = "pd-standard" 46 | } 47 | } 48 | 49 | network_interface { 50 | subnetwork = var.subnet-name 51 | access_config { 52 | // Ephemeral IP 53 | } 54 | } 55 | 56 | //network_interface { 57 | // network="default" 58 | //subnetwork = "${var.secondary-subnet-name}" 59 | // access_config { 60 | // Ephemeral IP 61 | // } 62 | // } 63 | 64 | attached_disk { 65 | source = "${var.deployment-name}-${var.environment}-${var.function}-${var.instancerole}-${var.instancenumber}-pd-standard" 66 | device_name = "appdata" 67 | } 68 | 69 | depends_on = [google_compute_disk.datadisk] 70 | 71 | tags = var.network-tag 72 | 73 | metadata = { 74 | environment = var.environment 75 | domain-name = var.domain-name 76 | function = var.function 77 | region = var.region 78 | keyring = var.keyring 79 | runtime-config = var.runtime-config 80 | kms-key = var.kms-key 81 | gcs-prefix = var.gcs-prefix 82 | netbios-name = var.netbios-name 83 | application = "basicwindows" 84 | windows-startup-script-ps1 = data.template_file.windowsstartup.rendered 85 | role = var.instancerole 86 | } 87 | 88 | service_account { 89 | //scopes = ["storage-ro","monitoring-write","logging-write","trace-append"] 90 | scopes = ["https://www.googleapis.com/auth/cloud-platform"] 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /modules/windowsWithStackdriver/vars.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | variable "machinetype" { 17 | type = string 18 | default = "n1-standard-4" 19 | } 20 | 21 | variable "osimage" { 22 | type = string 23 | } 24 | 25 | variable "environment" { 26 | type = string 27 | } 28 | 29 | variable "instancerole" { 30 | type = string 31 | default = "p" 32 | } 33 | 34 | variable "function" { 35 | type = string 36 | default = "pdc" 37 | } 38 | 39 | variable "instancenumber" { 40 | type = string 41 | default = "01" 42 | } 43 | 44 | variable "regionandzone" { 45 | type = string 46 | } 47 | 48 | variable "deployment-name" { 49 | type = string 50 | default = "" 51 | } 52 | 53 | variable "assignedsubnet" { 54 | type = string 55 | default = "default" 56 | } 57 | 58 | variable "domain-name" { 59 | type = string 60 | default = "test-domain" 61 | } 62 | 63 | variable "kms-key" { 64 | type = string 65 | default = "p@ssword" 66 | } 67 | 68 | variable "gcs-prefix" { 69 | type = string 70 | } 71 | 72 | variable "region" { 73 | type = string 74 | } 75 | 76 | variable "subnet-name" { 77 | type = string 78 | } 79 | 80 | variable "secondary-subnet-name" { 81 | type = string 82 | } 83 | 84 | variable "netbios-name" { 85 | type = string 86 | } 87 | 88 | variable "runtime-config" { 89 | type = string 90 | } 91 | 92 | variable "keyring" { 93 | type = string 94 | } 95 | 96 | variable "wait-on" { 97 | type = string 98 | } 99 | 100 | variable "domain-controller-address" { 101 | type = string 102 | } 103 | 104 | variable "network-tag" { 105 | type = list(string) 106 | default = [""] 107 | description = "network tags" 108 | } 109 | 110 | -------------------------------------------------------------------------------- /modules/windowsWithStackdriver/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/wsus/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | data "google_project" "project" {} 17 | data "template_file" "windowsstartupwsus" { 18 | template = "${file("../powershell/windowsstartupwsus.ps1")}" 19 | vars { 20 | environment = "${var.environment}" 21 | dnsserver1 = "${var.dnsserver1}" 22 | dnsserver2 = "${var.dnsserver2}" 23 | projectname = "${lower(data.google_project.project.name)}" 24 | number = "${var.instancenumber}" 25 | } 26 | } 27 | resource "google_compute_instance" "wsus"{ 28 | name = "${var.deployment-name}${var.environment}-${var.instancerole}-wsus${var.instancenumber}" 29 | machine_type = "${var.machinetype}" 30 | zone = "${var.regionandzone}" 31 | boot_disk { 32 | initialize_params 33 | { 34 | image = "${var.osimage}" 35 | size = "400" 36 | type = "pd-standard" 37 | } 38 | } 39 | network_interface { 40 | subnetwork = "${lower(data.google_project.project.name)}-vpc-${substr(var.regionandzone,0,length(var.regionandzone)-2)}-${var.assignedsubnet}" 41 | #subnetwork = "default" 42 | access_config { 43 | // Ephemeral IP 44 | } 45 | } 46 | tags = ["windowsupdate-server"] 47 | metadata { 48 | applicationstack="wsus" 49 | environment = "${var.environment}" 50 | application = "wsus" 51 | windows-startup-script-ps1 = "${data.template_file.windowsstartupwsus.rendered}" 52 | role = "${var.instancerole}" 53 | } 54 | service_account { 55 | scopes = ["storage-ro","monitoring-write","logging-write","trace-append"] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/wsus/vars.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | variable "machinetype" {type = "string" default = "n1-standard-4" } 17 | variable "osimage" {type = "string" default = "windows-server-2016-dc-v20180710"} 18 | variable "environment" {type = "string" } 19 | variable "instancerole" {type = "string" default = "p"} 20 | variable "instancenumber" {type = "string" default = "01"} 21 | variable "regionandzone" {type = "string"} 22 | variable "deployment-name" {type = "string" default = ""} 23 | variable "dnsserver1" {type = "string" default = ""} 24 | variable "dnsserver2" {type = "string" default = ""} 25 | -------------------------------------------------------------------------------- /powershell/bootstrap/domain-member.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Function Unwrap-SecureString() { 18 | Param( 19 | [System.Security.SecureString] $SecureString 20 | ) 21 | Return (New-Object -TypeName System.Net.NetworkCredential -ArgumentList '', $SecureString).Password 22 | } 23 | 24 | Function Set-RuntimeConfigVariable { 25 | Param( 26 | [Parameter(Mandatory=$True)][String] $ConfigPath, 27 | [Parameter(Mandatory=$True)][String] $Variable, 28 | [Parameter(Mandatory=$True)][String] $Text 29 | ) 30 | 31 | $Auth = $(gcloud auth print-access-token) 32 | 33 | $Path = "$ConfigPath/variables" 34 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$Path" 35 | 36 | $Json = (@{ 37 | name = "$Path/$Variable" 38 | text = $Text 39 | } | ConvertTo-Json) 40 | 41 | $Headers = @{ 42 | Authorization = "Bearer " + $Auth 43 | } 44 | 45 | $Params = @{ 46 | Method = "POST" 47 | Headers = $Headers 48 | ContentType = "application/json" 49 | Uri = $Url 50 | Body = $Json 51 | } 52 | 53 | Try { 54 | Return Invoke-RestMethod @Params 55 | } 56 | Catch { 57 | $Reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) 58 | $ErrResp = $Reader.ReadToEnd() | ConvertFrom-Json 59 | $Reader.Close() 60 | Return $ErrResp 61 | } 62 | 63 | } 64 | 65 | Function Get-RuntimeConfigWaiter { 66 | Param( 67 | [Parameter(Mandatory=$True)][String] $ConfigPath, 68 | [Parameter(Mandatory=$True)][String] $Waiter 69 | ) 70 | 71 | $Auth = $(gcloud auth print-access-token) 72 | 73 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 74 | $Headers = @{ 75 | Authorization = "Bearer " + $Auth 76 | } 77 | $Params = @{ 78 | Method = "GET" 79 | Headers = $Headers 80 | Uri = $Url 81 | } 82 | 83 | Return Invoke-RestMethod @Params 84 | } 85 | 86 | Function Wait-RuntimeConfigWaiter { 87 | Param( 88 | [Parameter(Mandatory=$True)][String] $ConfigPath, 89 | [Parameter(Mandatory=$True)][String] $Waiter, 90 | [int] $Sleep = 60 91 | ) 92 | $RuntimeWaiter = $Null 93 | While (($RuntimeWaiter -eq $Null) -Or (-Not $RuntimeWaiter.done)) { 94 | $RuntimeWaiter = Get-RuntimeConfigWaiter -ConfigPath $ConfigPath -Waiter $Waiter 95 | If (-Not $RuntimeWaiter.done) { 96 | Write-Host "Waiting for [$ConfigPath/waiters/$Waiter]..." 97 | Sleep $Sleep 98 | } 99 | } 100 | Return $RuntimeWaiter 101 | } 102 | 103 | Function New-RandomString { 104 | Param( 105 | [int] $Length = 10, 106 | [char[]] $AllowedChars = $Null 107 | ) 108 | If ($AllowedChars -eq $Null) { 109 | (,(33,126)) | % { For ($a=$_[0]; $a -le $_[1]; $a++) { $AllowedChars += ,[char][byte]$a } } 110 | } 111 | For ($i=1; $i -le $Length; $i++) { 112 | $Temp += ( $AllowedChars | Get-Random ) 113 | } 114 | Return $Temp 115 | } 116 | 117 | Function Create-RuntimeConfigWaiter { 118 | Param( 119 | [Parameter(Mandatory=$True)][String] $ConfigPath, 120 | [Parameter(Mandatory=$True)][String] $Waiter, 121 | [Parameter(Mandatory=$True)][String] $Timeout, 122 | [Parameter(Mandatory=$True)][String] $SuccessPath, 123 | [Parameter(Mandatory=$True)][Int] $SuccessCardinality, 124 | [Parameter(Mandatory=$False)][String] $FailurePath = "", 125 | [Parameter(Mandatory=$False)][Int] $FailureCardinality=0 126 | ) 127 | 128 | $RuntimeWaiter = $Null 129 | 130 | Write-Host $ConfigPath/waiters/$Waiter 131 | 132 | $Auth = $(gcloud auth print-access-token) 133 | 134 | 135 | if($FailurePath.Length -eq 0){ 136 | $Body = "{timeout: '" + $Timeout + "s', name: '$ConfigPath/waiters/$Waiter', success: { cardinality: { number: $SuccessCardinality, path: '$SuccessPath' }}}" 137 | }else{ 138 | $Body = "{timeout: '" + $Timeout + "s', name: '$ConfigPath/waiters/$Waiter', ` 139 | success: { cardinality: { number: $SuccessCardinality, path: '$SuccessPath' }}, ` 140 | failure: { cardinality: { number: $FailureCardinality, path: '$FailurePath' }}}" 141 | } 142 | 143 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters" 144 | 145 | $Headers = @{ 146 | Authorization="Bearer " + $Auth 147 | } 148 | $Params = @{ 149 | Method = "POST" 150 | Headers = $Headers 151 | Uri = $Url 152 | Body=$Body 153 | } 154 | Write-Host "$Url" 155 | # Write-Host "$Params" 156 | 157 | #Return Invoke-RestMethod $Params 158 | Return Invoke-RestMethod -Uri $Url -Headers $Headers -Method 'Post' -Body $Body -ContentType "application/json" 159 | #Return $RuntimeWaiter 160 | } 161 | 162 | Function Delete-RuntimeConfigWaiter { 163 | Param( 164 | [Parameter(Mandatory=$True)][String] $ConfigPath, 165 | [Parameter(Mandatory=$True)][String] $Waiter 166 | ) 167 | 168 | $RuntimeWaiter = $Null 169 | 170 | Write-Host $ConfigPath/waiters/$Waiter 171 | 172 | $Auth = $(gcloud auth print-access-token) 173 | 174 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 175 | 176 | $Headers = @{ 177 | Authorization="Bearer " + $Auth 178 | } 179 | 180 | Write-Host "$Url" 181 | 182 | Return Invoke-RestMethod -Uri $Url -Headers $Headers -Method 'Delete' 183 | } 184 | 185 | Function New-RandomPassword() { 186 | Param( 187 | [int] $Length = 16, 188 | [char[]] $AllowedChars = $Null 189 | ) 190 | Return New-RandomString -Length $Length -AllowedChars $AllowedChars | ConvertTo-SecureString -AsPlainText -Force 191 | } 192 | 193 | 194 | 195 | Function Get-GoogleMetadata() { 196 | Param ( 197 | [Parameter(Mandatory=$True)][String] $Path 198 | ) 199 | Try { 200 | Return Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/$Path 201 | } 202 | Catch { 203 | Return $Null 204 | } 205 | } 206 | 207 | 208 | Write-Host "Bootstrap script started..." 209 | 210 | 211 | $name = Get-GoogleMetadata "instance/name" 212 | $zone = Get-GoogleMetadata "instance/zone" 213 | 214 | If ("true" -like (Get-GoogleMetadata "instance/attributes/remove-address")) { 215 | Write-Host "Removing external address..." 216 | gcloud compute instances delete-access-config $name --zone $zone 217 | } 218 | 219 | 220 | Write-Host "Adding AD powershell tools..." 221 | Add-WindowsFeature RSAT-AD-PowerShell 222 | 223 | # the path to the runtime-config must be the full path ie. 224 | # "projects/{project-name}/configs/acme-runtime-config" 225 | # the success path is what is in wait-on 226 | # waiter name does not need to be passed in 227 | $Waiter = $name + '_waiter' 228 | 229 | $Deployment = Get-GoogleMetadata "instance/attributes/deployment-name" 230 | $SuccessPath = Get-GoogleMetadata "instance/attributes/wait-on" 231 | $ProjectId = Get-GoogleMetadata "/instance/attributes/project-id" 232 | $RuntimeConfig = Get-GoogleMetadata "instance/attributes/runtime-config" 233 | $FullRTConfigPath = "projects/$ProjectId/configs/$RuntimeConfig" 234 | 235 | Write-Host "Runtime-config waiter is $Waiter" 236 | Write-Host "Success path is $SuccessPath" 237 | Write-Host "Full runtime-config path is: $FullRTConfigPath" 238 | 239 | If ($SuccessPath) { 240 | Write-Host "Waiting for $SuccessPath..." 241 | Write-Host "Config $RuntimeConfig and waiter: $Waiter" 242 | 243 | $result = Create-RuntimeConfigWaiter $FullRTConfigPath ` 244 | $Waiter ` 245 | 1800 ` 246 | $SuccessPath ` 247 | 1 248 | 249 | Write-Host $result 250 | 251 | Wait-RuntimeConfigWaiter -ConfigPath $FullRTConfigPath -Waiter $Waiter 252 | } 253 | 254 | 255 | Write-Host "Configuring network..." 256 | $DomainControllerAddresses = Get-GoogleMetadata "instance/attributes/domain-controller-address" 257 | # set dns to domain controller 258 | Set-DnsClientServerAddress -InterfaceAlias Ethernet -ServerAddresses $DomainControllerAddresses 259 | 260 | Write-Host "Configuring local admin..." 261 | # startup script runs as local system which cannot join domain 262 | # so do the join as local administrator using random password 263 | $LocalAdminPassword = New-RandomPassword 264 | Set-LocalUser Administrator -Password $LocalAdminPassword 265 | Enable-LocalUser Administrator 266 | 267 | $LocalAdminCredentials = New-Object ` 268 | -TypeName System.Management.Automation.PSCredential ` 269 | -ArgumentList "\Administrator",$LocalAdminPassword 270 | Invoke-Command -Credential $LocalAdminCredentials -ComputerName . -ScriptBlock { 271 | 272 | Write-Host "Getting job metadata..." 273 | $Domain = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/domain-name 274 | $NetBiosName = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/netbios-name 275 | $KmsKey = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/kms-key 276 | $KmsRegion = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring-region 277 | $Region = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/region 278 | $Keyring = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring 279 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 280 | 281 | Write-Host "Fetching admin credentials..." 282 | 283 | # fetch domain admin credentials 284 | If ($GcsPrefix.EndsWith("/")) { 285 | $GcsPrefix = $GcsPrefix -Replace ".$" 286 | } 287 | $TempFile = New-TemporaryFile 288 | 289 | # invoke-command sees gsutil output as an error so redirect stderr to stdout and stringify to suppress 290 | gsutil cp $GcsPrefix/output/domain-admin-password.bin $TempFile.FullName 2>&1 | %{ "$_" } 291 | 292 | $DomainAdminPassword = $(gcloud kms decrypt --key $KmsKey --location $KmsRegion --keyring $Keyring --ciphertext-file $TempFile.FullName --plaintext-file - | ConvertTo-SecureString -AsPlainText -Force) 293 | 294 | Remove-Item $TempFile.FullName 295 | 296 | <#$DomainAdminCredentials = New-Object ` 297 | -TypeName System.Management.Automation.PSCredential ` 298 | -ArgumentList "$NetBiosName\Administrator",$DomainAdminPassword#> 299 | Write-Host "Domain is $Domain" 300 | 301 | $DomainAdminCredentials = New-Object ` 302 | -TypeName System.Management.Automation.PSCredential ` 303 | -ArgumentList "$Domain\Administrator", $DomainAdminPassword 304 | 305 | Write-Host "Joining domain... using credential $DomainAdminCredentials" 306 | Add-Computer -DomainName $Domain -Credential $DomainAdminCredentials 307 | 308 | $RuntimeConfig = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/runtime-config 309 | Write-Host "Runtime config is $RuntimeConfig" 310 | 311 | #Now write the status-config-url for the c2d scripts 312 | #It needs to be the full path 313 | $Script:run_time_base = 'https://runtimeconfig.googleapis.com/v1beta1' 314 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 315 | $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 316 | $projectId = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/project-id 317 | 318 | gcloud compute instances add-metadata "$name" --zone $zone --metadata "status-config-url=$Script:run_time_base/projects/$projectId/configs/$RuntimeConfig" 319 | 320 | Write-Host "setting status-config-url=$Script:run_time_base/projects/$projectId/configs/$RuntimeConfig" 321 | 322 | try { 323 | Write-Host "Done adding to domain, adding key to reg: HKLM:\SOFTWARE\Google\SQLOnDomain" 324 | $result = New-Item -Path "HKLM:\SOFTWARE\Google\SQLOnDomain" -Force 325 | } 326 | catch [System.IO.IOException] { 327 | Write-Log "$_.Exception.Message" 328 | Write-Log "Error writing to registry $result" 329 | } 330 | } 331 | 332 | 333 | $PostJoinScriptUrl = Get-GoogleMetadata "instance/attributes/post-join-script-url" 334 | If ($PostJoinScriptUrl) { 335 | 336 | Write-Host "Configuring startup metadata for post-join script..." 337 | # set post join url as startup script then restart 338 | $name = Get-GoogleMetadata "instance/name" 339 | $zone = Get-GoogleMetadata "instance/zone" 340 | gcloud compute instances add-metadata "$name" --zone $zone --metadata "windows-startup-script-url=$PostJoinScriptUrl" 341 | 342 | Write-Host "Restarting..." 343 | Restart-Computer 344 | 345 | } 346 | Else { 347 | 348 | Write-Host "Configuring startup metadata..." 349 | # remove startup script from metadata to prevent rerun on reboot 350 | $name = Get-GoogleMetadata "instance/name" 351 | $zone = Get-GoogleMetadata "instance/zone" 352 | gcloud compute instances remove-metadata "$name" --zone $zone --keys windows-startup-script-url 353 | 354 | Write-Host "Signaling completion..." 355 | 356 | # flag completion of bootstrap requires beta gcloud component 357 | $name = Get-GoogleMetadata "instance/name" 358 | $RuntimeConfig = Get-GoogleMetadata "instance/attributes/runtime-config" 359 | 360 | Set-RuntimeConfigVariable -ConfigPath $RuntimeConfig -Variable bootstrap/$name/success/time -Text (Get-Date -Format g) 361 | 362 | } 363 | -------------------------------------------------------------------------------- /powershell/bootstrap/initial_alwayson_startup_script.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | ####This script is copied into the end of install_sql_server_principal-step_1.ps1 19 | Set-StrictMode -Version Latest 20 | 21 | $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 22 | 23 | # Import Modules 24 | try { 25 | Import-Module $script:gce_install_dir\gce_base.psm1 -ErrorAction Stop 26 | } 27 | catch [System.Management.Automation.ActionPreferenceStopException] { 28 | Write-Host $_.Exception.GetBaseException().Message 29 | Write-Host ("Unable to import GCE module from $script:gce_install_dir. " + 30 | 'Check error message, or ensure module is present.') 31 | exit 2 32 | } 33 | 34 | # Default Values 35 | $Script:c2d_scripts_bucket = 'c2d-windows/scripts' 36 | $Script:tf_scripts_bucket = '{bucket}/powershell/bootstrap' 37 | $Script:install_path="C:\C2D" # Folder for downloads 38 | $script:show_msgs = $false 39 | $script:write_to_serial = $false 40 | 41 | 42 | # Functions 43 | function DownloadScript { 44 | <# 45 | .SYNOPSIS 46 | Downloads a script to the localmachine from GCS. 47 | .DESCRIPTION 48 | Uses WebClient to download a script file. 49 | .EXAMPLE 50 | DownloadScript -path bucket/.. -filename 51 | #> 52 | param ( 53 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 54 | $path, 55 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 56 | $filename, 57 | [Switch] $overwrite 58 | ) 59 | $storage_url = 'http://storage.googleapis.com' 60 | $download_url = "$storage_url/$path" 61 | 62 | 63 | # Check if file already exists and act accordingly. 64 | if ((Test-path -path $filename)){ 65 | if ($overwrite){ 66 | Write-Log "$filename already exists. Overwrite flag set." 67 | _DeleteFiles -files $filename 68 | } 69 | else { 70 | Write-Log "$filename already exists. Overwrite flag notset." 71 | return $true 72 | } 73 | } 74 | # Download the file 75 | Write-Log "Original download url: $download_url" 76 | # To avoid cache issues 77 | $url = $download_url + "?random=" + (Get-Random).ToString() 78 | 79 | Write-Log "Downloading $url to $filename" 80 | try { 81 | Invoke-WebRequest -Uri $url -OutFile $filename -Headers @{"Cache-Control"="private"} 82 | } 83 | catch [System.Net.WebException] { 84 | $response = $_.Exception.Response 85 | if ($response) { 86 | _PrintError 87 | Write-Log $response.StatusCode -error # This is a System.Net.HttpStatusCode enum value 88 | Write-Log $response.StatusCode.value__ -error # This is the numeric version. 89 | } 90 | else { 91 | $type = $_.Exception.GetType().FullName 92 | $message = $_.Exception.Message 93 | Write-Log "$type $message" 94 | } 95 | return $false 96 | } 97 | 98 | # Check if download successfull 99 | if ((Test-path -path $filename)){ 100 | return $true 101 | } 102 | else { 103 | Write-Log "File not found." 104 | return $false 105 | } 106 | } 107 | 108 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 109 | $script_bucket = $Script:tf_scripts_bucket.replace("{bucket}", $GcsPrefix) 110 | 111 | Write-Log "Looking for scripts in $script_bucket in initial-alwayon-startup-script" 112 | 113 | ## Main 114 | # Instance specific variables 115 | $script_name = 'sql_install.ps1' 116 | $script_subpath = 'sqlserver' 117 | $task_name = "SQLInstall" 118 | 119 | # Create the C:\C2D folder 120 | if (!(Test-path -path $Script:install_path )) { 121 | try { 122 | New-Item -ItemType directory -Path $Script:install_path 123 | } 124 | catch { 125 | _PrintError 126 | exit 1 127 | } 128 | } 129 | 130 | # Download the scripts 131 | # Base Script 132 | $base_script_path = "$Script:c2d_scripts_bucket/c2d_base.psm1" 133 | $base_script = "$Script:install_path\c2d_base.psm1" 134 | if (DownloadScript -path $base_script_path -filename $base_script) { 135 | Write-Log "File downloaded successfully." 136 | } 137 | else { 138 | Write-Log "File not found." 139 | exit 2 140 | } 141 | 142 | # Copy Run Script down 143 | $run_script = "$Script:install_path\$script_name" 144 | $run_script_path = "$script_bucket/$script_name" 145 | Write-Log "run_script_path is $run_script_path" 146 | gsutil cp $run_script_path $run_script 2>&1 | %{ "$_" } 147 | Write-Log "Scripts copied down" 148 | 149 | # Execute the script 150 | Write-Log "Checking if $task_name sctask exists?" 151 | $sc_task = Get-ScheduledTask -TaskName $task_name -ErrorAction SilentlyContinue 152 | if ($sc_task) { 153 | Write-Log "$task_name schtask exists." 154 | try { 155 | Write-Log "-- Executing sctask $task_name. --" 156 | $response = Start-ScheduledTask -TaskName $task_name 157 | Write-Log $response 158 | 159 | } 160 | catch { 161 | $type = $_.Exception.GetType().FullName 162 | $message = $_.Exception.Message 163 | Write-Log "$type $message" 164 | exit 1 165 | } 166 | } 167 | else { 168 | Write-Log "schtask $task_name does not exists." 169 | Write-Log "Executing: $run_script" 170 | try { 171 | & $run_script -task_name $task_name 172 | } 173 | catch { 174 | $type = $_.Exception.GetType().FullName 175 | $message = $_.Exception.Message 176 | Write-Log "$type $message" 177 | exit 1 178 | } 179 | } -------------------------------------------------------------------------------- /powershell/bootstrap/install-sql-server-principal-step-1.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Function Set-RuntimeConfigVariable { 18 | Param( 19 | [Parameter(Mandatory=$True)][String] $ConfigPath, 20 | [Parameter(Mandatory=$True)][String] $Variable, 21 | [Parameter(Mandatory=$True)][String] $Text 22 | ) 23 | 24 | $Auth = $(gcloud auth print-access-token) 25 | 26 | $Path = "$ConfigPath/variables" 27 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$Path" 28 | 29 | $Json = (@{ 30 | name = "$Path/$Variable" 31 | text = $Text 32 | } | ConvertTo-Json) 33 | 34 | $Headers = @{ 35 | Authorization = "Bearer " + $Auth 36 | } 37 | 38 | $Params = @{ 39 | Method = "POST" 40 | Headers = $Headers 41 | ContentType = "application/json" 42 | Uri = $Url 43 | Body = $Json 44 | } 45 | 46 | Try { 47 | Return Invoke-RestMethod @Params 48 | } 49 | Catch { 50 | $Reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) 51 | $ErrResp = $Reader.ReadToEnd() | ConvertFrom-Json 52 | $Reader.Close() 53 | Return $ErrResp 54 | } 55 | 56 | } 57 | 58 | Function Get-RuntimeConfigWaiter { 59 | Param( 60 | [Parameter(Mandatory=$True)][String] $ConfigPath, 61 | [Parameter(Mandatory=$True)][String] $Waiter 62 | ) 63 | 64 | $Auth = $(gcloud auth print-access-token) 65 | 66 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 67 | $Headers = @{ 68 | Authorization = "Bearer " + $Auth 69 | } 70 | $Params = @{ 71 | Method = "GET" 72 | Headers = $Headers 73 | Uri = $Url 74 | } 75 | 76 | Return Invoke-RestMethod @Params 77 | } 78 | 79 | Function Wait-RuntimeConfigWaiter { 80 | Param( 81 | [Parameter(Mandatory=$True)][String] $ConfigPath, 82 | [Parameter(Mandatory=$True)][String] $Waiter, 83 | [int] $Sleep = 60 84 | ) 85 | $RuntimeWaiter = $Null 86 | While (($RuntimeWaiter -eq $Null) -Or (-Not $RuntimeWaiter.done)) { 87 | $RuntimeWaiter = Get-RuntimeConfigWaiter -ConfigPath $ConfigPath -Waiter $Waiter 88 | If (-Not $RuntimeWaiter.done) { 89 | Write-Host "Waiting for [$ConfigPath/waiters/$Waiter]..." 90 | Sleep $Sleep 91 | } 92 | } 93 | Return $RuntimeWaiter 94 | } 95 | 96 | 97 | Function Get-GoogleMetadata() { 98 | Param ( 99 | [Parameter(Mandatory=$True)][String] $Path 100 | ) 101 | Try { 102 | Return Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/$Path 103 | } 104 | Catch { 105 | Return $Null 106 | } 107 | } 108 | 109 | 110 | Write-Host "Prepare principal SQL Server script started..." 111 | 112 | $name = Get-GoogleMetadata "instance/name" 113 | $zone = Get-GoogleMetadata "instance/zone" 114 | 115 | If ("true" -like (Get-GoogleMetadata "instance/attributes/remove-address")) { 116 | Write-Host "Removing external address..." 117 | gcloud compute instances delete-access-config $name --zone $zone 118 | } 119 | 120 | 121 | Write-Output "Fetching metadata parameters..." 122 | 123 | $DomainControllerAddress = Get-GoogleMetadata "instance/attributes/domain-controller-address" 124 | $Domain = Get-GoogleMetadata "instance/attributes/domain-name" 125 | $NetBiosName = Get-GoogleMetadata "/instance/attributes/netbios-name" 126 | $KmsKey = Get-GoogleMetadata "instance/attributes/kms-key" 127 | $GcsPrefix = Get-GoogleMetadata "instance/attributes/gcs-prefix" 128 | $RuntimeConfig = Get-GoogleMetadata "instance/attributes/runtime-config" 129 | $SuccessPath = Get-GoogleMetadata "instance/attributes/wait-on" 130 | $ProjectId = Get-GoogleMetadata "instance/attributes/project-id" 131 | $FullRTConfigPath = "projects/$ProjectId/configs/$RuntimeConfig" 132 | 133 | # the path to the runtime-config must be the full path ie. 134 | # "projects/{project-name}/configs/acme-runtime-config" 135 | # the success path is what is in wait-on 136 | # waiter name does not need to be passed in 137 | $Waiter = $name + '_waiter' 138 | 139 | ## remaining script has external dependencies, so invoke waiter before continuing 140 | Write-Host "Waiting on $Waiter" 141 | Wait-RuntimeConfigWaiter -ConfigPath $FullRTConfigPath -Waiter $Waiter 142 | Write-Host "Waiting completed ... $Waiter" 143 | Write-Host "Configuring network..." 144 | 145 | 146 | # This is the new part all credit due to click to deploy team 147 | 148 | Set-StrictMode -Version Latest 149 | 150 | $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 151 | 152 | # Import Modules 153 | try { 154 | Import-Module $script:gce_install_dir\gce_base.psm1 -ErrorAction Stop 155 | } 156 | catch [System.Management.Automation.ActionPreferenceStopException] { 157 | Write-Host $_.Exception.GetBaseException().Message 158 | Write-Host ("Unable to import GCE module from $script:gce_install_dir. " + 159 | 'Check error message, or ensure module is present.') 160 | exit 2 161 | } 162 | 163 | # Default Values 164 | $Script:c2d_scripts_bucket = 'c2d-windows/scripts' 165 | #$Script:tf_scripts_bucket = 'gs://acme-deployment/powershell/bootstrap' 166 | $Script:tf_scripts_bucket = '{bucket}/powershell/bootstrap' 167 | $Script:install_path="C:\C2D" # Folder for downloads 168 | $script:show_msgs = $false 169 | $script:write_to_serial = $false 170 | 171 | 172 | # Functions 173 | function DownloadScript { 174 | <# 175 | .SYNOPSIS 176 | Downloads a script to the localmachine from GCS. 177 | .DESCRIPTION 178 | Uses WebClient to download a script file. 179 | .EXAMPLE 180 | DownloadScript -path bucket/.. -filename 181 | #> 182 | param ( 183 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 184 | $path, 185 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 186 | $filename, 187 | [Switch] $overwrite 188 | ) 189 | $storage_url = 'http://storage.googleapis.com' 190 | $download_url = "$storage_url/$path" 191 | 192 | 193 | # Check if file already exists and act accordingly. 194 | if ((Test-path -path $filename)){ 195 | if ($overwrite){ 196 | Write-Log "$filename already exists. Overwrite flag set." 197 | _DeleteFiles -files $filename 198 | } 199 | else { 200 | Write-Log "$filename already exists. Overwrite flag notset." 201 | return $true 202 | } 203 | } 204 | # Download the file 205 | Write-Log "Original download url: $download_url" 206 | # To avoid cache issues 207 | $url = $download_url + "?random=" + (Get-Random).ToString() 208 | 209 | Write-Log "Downloading $url to $filename" 210 | try { 211 | Invoke-WebRequest -Uri $url -OutFile $filename -Headers @{"Cache-Control"="private"} 212 | } 213 | catch [System.Net.WebException] { 214 | $response = $_.Exception.Response 215 | if ($response) { 216 | _PrintError 217 | Write-Log $response.StatusCode -error # This is a System.Net.HttpStatusCode enum value 218 | Write-Log $response.StatusCode.value__ -error # This is the numeric version. 219 | } 220 | else { 221 | $type = $_.Exception.GetType().FullName 222 | $message = $_.Exception.Message 223 | Write-Log "$type $message" 224 | } 225 | return $false 226 | } 227 | 228 | # Check if download successfull 229 | if ((Test-path -path $filename)){ 230 | return $true 231 | } 232 | else { 233 | Write-Log "File not found." 234 | return $false 235 | } 236 | } 237 | 238 | 239 | ## Main 240 | # Instance specific variables 241 | $script_name = 'sql_install.ps1' 242 | $script_subpath = 'sqlserver' 243 | $task_name = "SQLInstall" 244 | 245 | # Create the C:\C2D folder 246 | if (!(Test-path -path $Script:install_path )) { 247 | try { 248 | New-Item -ItemType directory -Path $Script:install_path 249 | } 250 | catch { 251 | _PrintError 252 | exit 1 253 | } 254 | } 255 | 256 | # Download the scripts 257 | # Base Script 258 | $base_script_path = "$Script:c2d_scripts_bucket/c2d_base.psm1" 259 | $base_script = "$Script:install_path\c2d_base.psm1" 260 | if (DownloadScript -path $base_script_path -filename $base_script) { 261 | Write-Log "File downloaded successfully." 262 | } 263 | else { 264 | Write-Log "File not found." 265 | exit 2 266 | } 267 | 268 | 269 | # Copy Run Script down from our bucket (Not c2d) 270 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 271 | $script_bucket = $Script:tf_scripts_bucket.replace("{bucket}", $GcsPrefix) 272 | 273 | Write-Log "Looking for scripts in $script_bucket in initial-sql-server-principal.ps1" 274 | 275 | $run_script = "$Script:install_path\$script_name" 276 | $run_script_path = "$script_bucket/$script_name" 277 | gsutil cp $run_script_path $run_script 2>&1 | %{ "$_" } 278 | 279 | 280 | # Execute the script 281 | Write-Log "Checking if $task_name sctask exists?" 282 | $sc_task = Get-ScheduledTask -TaskName $task_name -ErrorAction SilentlyContinue 283 | if ($sc_task) { 284 | Write-Log "$task_name schtask exists." 285 | try { 286 | Write-Log "-- Executing sctask $task_name. --" 287 | $response = Start-ScheduledTask -TaskName $task_name 288 | Write-Log $response 289 | 290 | } 291 | catch { 292 | $type = $_.Exception.GetType().FullName 293 | $message = $_.Exception.Message 294 | Write-Log "$type $message" 295 | exit 1 296 | } 297 | } 298 | else { 299 | Write-Log "schtask $task_name does not exists." 300 | Write-Log "Executing: $run_script" 301 | try { 302 | & $run_script -task_name $task_name 303 | } 304 | catch { 305 | $type = $_.Exception.GetType().FullName 306 | $message = $_.Exception.Message 307 | Write-Log "$type $message" 308 | exit 1 309 | } 310 | } 311 | 312 | Write-Host "SQL script completed. Removing from metadata..." 313 | # remove startup script from metadata to prevent rerun on reboot 314 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 315 | $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 316 | gcloud compute instances remove-metadata "$name" --zone $zone --keys windows-startup-script-url 317 | 318 | Write-Host "Signaling completion..." 319 | -------------------------------------------------------------------------------- /powershell/bootstrap/primary-domain-controller-step-1.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Function New-RandomString { 18 | Param( 19 | [int] $Length = 10, 20 | [char[]] $AllowedChars = $Null 21 | ) 22 | If ($AllowedChars -eq $Null) { 23 | (,(33,126)) | % { For ($a=$_[0]; $a -le $_[1]; $a++) { $AllowedChars += ,[char][byte]$a } } 24 | } 25 | For ($i=1; $i -le $Length; $i++) { 26 | $Temp += ( $AllowedChars | Get-Random ) 27 | } 28 | Return $Temp 29 | } 30 | Function New-RandomPassword() { 31 | Param( 32 | [int] $Length = 16, 33 | [char[]] $AllowedChars = $Null 34 | ) 35 | Return New-RandomString -Length $Length -AllowedChars $AllowedChars | ConvertTo-SecureString -AsPlainText -Force 36 | } 37 | Function Unwrap-SecureString() { 38 | Param( 39 | [System.Security.SecureString] $SecureString 40 | ) 41 | Return (New-Object -TypeName System.Net.NetworkCredential -ArgumentList '', $SecureString).Password 42 | } 43 | 44 | Function Get-GoogleMetadata() { 45 | Param ( 46 | [Parameter(Mandatory=$True)][String] $Path 47 | ) 48 | Try { 49 | Return Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/$Path 50 | } 51 | Catch { 52 | Return $Null 53 | } 54 | } 55 | 56 | 57 | Write-Host "Bootstrap script started..." 58 | 59 | 60 | #Write-Host "Installing AD features in background..." 61 | #Start-Job -ScriptBlock { Install-WindowsFeature -name AD-Domain-Services -IncludeManagementTools } 62 | Write-Host "Installing AD features..." 63 | Install-WindowsFeature -name AD-Domain-Services -IncludeManagementTools 64 | 65 | 66 | #Write-Host "Removing external address in background..." 67 | #Start-Job -ScriptBlock { 68 | # # windows should have activated before script is invoked, so now remove external address 69 | # $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 70 | # $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 71 | # gcloud compute instances delete-access-config $name --zone $zone 72 | #} 73 | 74 | If ("true" -like (Get-GoogleMetadata "instance/attributes/remove-address")) { 75 | Write-Host "Removing external address..." 76 | $name = Get-GoogleMetadata "instance/name" 77 | $zone = Get-GoogleMetadata "instance/zone" 78 | gcloud compute instances delete-access-config $name --zone $zone 79 | } 80 | 81 | 82 | Write-Host "Configuring network..." 83 | # reconfigure dhcp address as static to avoid warnings during dcpromo 84 | $IpAddr = Get-NetIPAddress -InterfaceAlias Ethernet -AddressFamily IPv4 85 | $IpConf = Get-NetIPConfiguration -InterfaceAlias Ethernet 86 | Set-NetIPInterface ` 87 | -InterfaceAlias Ethernet ` 88 | -Dhcp Disabled 89 | New-NetIPAddress ` 90 | -InterfaceAlias Ethernet ` 91 | -IPAddress $IpAddr.IPAddress ` 92 | -AddressFamily IPv4 ` 93 | -PrefixLength $IpAddr.PrefixLength ` 94 | -DefaultGateway $IpConf.IPv4DefaultGateway.NextHop 95 | 96 | # set dns to google cloud default, will be set to loopback once dns feature is installed 97 | Set-DnsClientServerAddress -InterfaceAlias Ethernet -ServerAddresses $IpConf.IPv4DefaultGateway.NextHop 98 | 99 | # above can cause network blip, so wait until metadata server is responsive 100 | $HaveMetadata = $False 101 | While( ! $HaveMetadata ) { Try { 102 | Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/ 1>$Null 2>&1 103 | $HaveMetadata = $True 104 | } Catch { 105 | Write-Host "Waiting on metadata..." 106 | Start-Sleep 5 107 | } } 108 | Write-Host "Contacted metadata server. Proceeding..." 109 | 110 | 111 | Write-Host "Fetching metadata parameters..." 112 | $Domain = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/domain-name 113 | $NetBiosName = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/netbios-name 114 | $KmsKey = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/kms-key 115 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 116 | $Region = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/region 117 | $KmsRegion = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring-region 118 | $Keyring = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring 119 | #$RuntimeConfig = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/runtime-config 120 | 121 | Write-Host "KMS Key has been fetched" 122 | 123 | Write-Host "Configuring admin credentials..." 124 | $SafeModeAdminPassword = New-RandomPassword 125 | $LocalAdminPassword = New-RandomPassword 126 | 127 | Set-LocalUser Administrator -Password $LocalAdminPassword 128 | Enable-LocalUser Administrator 129 | 130 | Write-Host "Saving encrypted credentials in GCS..." 131 | If ($GcsPrefix.EndsWith("/")) { 132 | $GcsPrefix = $GcsPrefix -Replace ".$" 133 | } 134 | $TempFile = New-TemporaryFile 135 | 136 | Unwrap-SecureString $LocalAdminPassword | gcloud kms encrypt --key $KmsKey --plaintext-file - --ciphertext-file $TempFile.FullName --location $KmsRegion --keyring $Keyring 137 | gsutil cp $TempFile.FullName "$GcsPrefix/output/domain-admin-password.bin" 138 | 139 | Unwrap-SecureString $SafeModeAdminPassword | gcloud kms encrypt --key $KmsKey --plaintext-file - --ciphertext-file $TempFile.FullName --location $KmsRegion --keyring $Keyring 140 | gsutil cp $TempFile.FullName "$GcsPrefix/output/dsrm-admin-password.bin" 141 | 142 | Remove-Item $TempFile.FullName -Force 143 | 144 | Write-Host "Waiting for background jobs..." 145 | Get-Job | Wait-Job 146 | 147 | 148 | Write-Host "Creating AD forest..." 149 | 150 | $Params = @{ 151 | DomainName = $Domain 152 | DomainNetbiosName = $NetBiosName 153 | InstallDNS = $True 154 | NoRebootOnCompletion = $True 155 | SafeModeAdministratorPassword = $SafeModeAdminPassword 156 | Force = $True 157 | } 158 | Install-ADDSForest @Params 159 | 160 | 161 | Write-Host "Configuring startup metadata..." 162 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 163 | $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 164 | gcloud compute instances add-metadata "$name" --zone $zone --metadata windows-startup-script-url="$GcsPrefix/powershell/bootstrap/primary-domain-controller-step-2.ps1" 165 | 166 | 167 | Write-Host "Restarting computer after step 1 ..." 168 | 169 | Restart-Computer 170 | -------------------------------------------------------------------------------- /powershell/bootstrap/primary-domain-controller-step-2.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Function Set-RuntimeConfigVariable { 18 | Param( 19 | [Parameter(Mandatory=$True)][String] $ConfigPath, 20 | [Parameter(Mandatory=$True)][String] $Variable, 21 | [Parameter(Mandatory=$True)][String] $Text 22 | ) 23 | 24 | $Auth = $(gcloud auth print-access-token) 25 | 26 | $Path = "$ConfigPath/variables" 27 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$Path" 28 | 29 | $Json = (@{ 30 | name = "$Path/$Variable" 31 | text = $Text 32 | } | ConvertTo-Json) 33 | 34 | $Headers = @{ 35 | Authorization = "Bearer " + $Auth 36 | } 37 | 38 | $Params = @{ 39 | Method = "POST" 40 | Headers = $Headers 41 | ContentType = "application/json" 42 | Uri = $Url 43 | Body = $Json 44 | } 45 | 46 | Try { 47 | Return Invoke-RestMethod @Params 48 | } 49 | Catch { 50 | $Reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) 51 | $ErrResp = $Reader.ReadToEnd() | ConvertFrom-Json 52 | $Reader.Close() 53 | Return $ErrResp 54 | } 55 | 56 | } 57 | 58 | Function Get-RuntimeConfigWaiter { 59 | Param( 60 | [Parameter(Mandatory=$True)][String] $ConfigPath, 61 | [Parameter(Mandatory=$True)][String] $Waiter 62 | ) 63 | 64 | $Auth = $(gcloud auth print-access-token) 65 | 66 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 67 | $Headers = @{ 68 | Authorization = "Bearer " + $Auth 69 | } 70 | $Params = @{ 71 | Method = "GET" 72 | Headers = $Headers 73 | Uri = $Url 74 | } 75 | 76 | Return Invoke-RestMethod @Params 77 | } 78 | 79 | Function Wait-RuntimeConfigWaiter { 80 | Param( 81 | [Parameter(Mandatory=$True)][String] $ConfigPath, 82 | [Parameter(Mandatory=$True)][String] $Waiter, 83 | [int] $Sleep = 60 84 | ) 85 | $RuntimeWaiter = $Null 86 | While (($RuntimeWaiter -eq $Null) -Or (-Not $RuntimeWaiter.done)) { 87 | $RuntimeWaiter = Get-RuntimeConfigWaiter -ConfigPath $ConfigPath -Waiter $Waiter 88 | If (-Not $RuntimeWaiter.done) { 89 | Write-Host "Waiting for [$ConfigPath/waiters/$Waiter]..." 90 | Sleep $Sleep 91 | } 92 | } 93 | Return $RuntimeWaiter 94 | } 95 | 96 | 97 | Write-Host "Runtime-config script started..." 98 | 99 | 100 | Write-Host "Configuring NTP..." 101 | # use google internal time server 102 | w32tm /config /manualpeerlist:"metadata.google.internal" /syncfromflags:manual /reliable:yes /update 103 | 104 | 105 | ## download and run user creation script 106 | #$GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 107 | #$TempFile = New-TemporaryFile 108 | #$TempFile.MoveTo($TempFile.fullName + ".ps1") 109 | #gsutil cp $GcsPrefix/bootstrap/create-domain-users.ps1 $TempFile.FullName#Invoke-Expression $TempFile.FullName 110 | #Remove-Item $TempFile.FullName -Force 111 | 112 | 113 | # poll domain controller until it appears ready 114 | Do { 115 | Try { 116 | $test = Get-ADDomain 117 | } 118 | Catch { 119 | Write-Host "Waiting for DC to become available..." 120 | Sleep 15 121 | } 122 | } 123 | Until ($test) 124 | 125 | 126 | Write-Host "Configuring startup metadata..." 127 | # remove startup script from metadata to prevent rerun on reboot 128 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 129 | $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 130 | gcloud compute instances remove-metadata "$name" --zone $zone --keys windows-startup-script-url 131 | 132 | Write-Host "Signaling completion..." 133 | 134 | # flag completion of bootstrap requires beta gcloud component 135 | $projectId = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/project-id 136 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 137 | $RuntimeConfig = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/runtime-config 138 | $deploymentName = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/deployment-name 139 | $statusPath = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/status-variable-path 140 | 141 | Write-Host "Config is at: projects/$projectId/configs/$RuntimeConfig" 142 | Write-Host "Variable is: bootstrap/$deploymentName/$statusPath/success/time" 143 | 144 | Set-RuntimeConfigVariable -ConfigPath "projects/$projectId/configs/$RuntimeConfig" -Variable bootstrap/$deploymentName/$statusPath/success/time -Text (Get-Date -Format g) 145 | 146 | Write-Host "Step 2 completed" 147 | -------------------------------------------------------------------------------- /powershell/c2d/c2d_base.psm1: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | <# 16 | .SYNOPSIS 17 | C2D Base Modules. 18 | .DESCRIPTION 19 | Base modules needed for C2D Powershell scripts to run scripts to run. 20 | .NOTES 21 | LastModifiedDate: $Date: 2017/02/26 $ 22 | Version: $Revision: #2 $ 23 | 24 | #requires -version 3.0 25 | #> 26 | 27 | 28 | $Script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 29 | $Script:run_time_base = 'https://runtimeconfig.googleapis.com/v1beta1' 30 | 31 | # Import Modules 32 | try { 33 | Import-Module $script:gce_install_dir\gce_base.psm1 -ErrorAction Stop 34 | } 35 | catch [System.Management.Automation.ActionPreferenceStopException] { 36 | Write-Host $_.Exception.GetBaseException().Message 37 | Write-Host ("Unable to import GCE module from $script:gce_install_dir. " + 38 | 'Check error message, or ensure module is present.') 39 | exit 2 40 | } 41 | 42 | 43 | # Functions 44 | function _CreateRunTimeConfig { 45 | <# 46 | .SYNOPSIS 47 | Create a new run-time config. 48 | .DESCRIPTION 49 | Generate new run-time config for a given instance. 50 | This will be separate from the deployment manager instance config. 51 | .EXAMPLE 52 | _CreateRunTimeConfig 53 | #> 54 | param ( 55 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 56 | $config_name 57 | ) 58 | 59 | $project_id = _FetchFromMetaData -property 'project-id' -project_only 60 | $run_time_path = "projects/$project_id/configs" 61 | $newConfig = @{ 62 | name = "$run_time_path/$config_name" 63 | } 64 | 65 | $json = $newConfig | ConvertTo-Json 66 | Write-Log "Writing a new startup watcher config: $json" 67 | 68 | # TODO: Need to add a check for exisiting path. 69 | if (_RunTimePost -path "$run_time_path/" -post $json) { 70 | return "$run_time_path/$config_name" 71 | } 72 | else { 73 | Write-Log "Failed to create ConfigName: $config_name" -error 74 | } 75 | } 76 | 77 | function _GetRuntimeConfig { 78 | <# 79 | .SYNOPSIS 80 | Fetched run-time config. 81 | .DESCRIPTION 82 | Get the URL for runtime config from the metadataserver 83 | .EXAMPLE 84 | _GetRuntimeConfig 85 | #> 86 | 87 | $config_name = $null 88 | # Get RuntimeConfig URL for the deployment 89 | $runtime_config = _FetchFromMetaData -property 'attributes/status-config-url' 90 | 91 | if ($runtime_config) { 92 | # Use second part of the config URL 93 | $config_name = (($runtime_config -split "$Script:run_time_base/")[1]) 94 | return $config_name 95 | } 96 | else { 97 | Write-Log 'No RunTimeConfig found URL found in metadata.' -error 98 | return $false 99 | } 100 | } 101 | 102 | function _RunTimeQuery{ 103 | <# 104 | .SYNOPSIS 105 | Do a POST/GET request 106 | .DESCRIPTION 107 | Is a sub function called to do POST request 108 | .EXAMPLE 109 | _RunTimeQuery -get -path 110 | .EXAMPLE 111 | _RunTimeQuery -post -path -body 112 | #> 113 | param ( 114 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 115 | $access_token, 116 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 117 | $path, 118 | [Alias('get')] 119 | [Switch] $get_req, 120 | [Alias('post')] 121 | [Switch] $post_req, 122 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 123 | $body 124 | ) 125 | 126 | # Define URL 127 | $url = "$Script:run_time_base/$path" 128 | $header = @{"Authorization"="Bearer $access_token"} 129 | 130 | if ($get_req) { 131 | try { 132 | # GET request against a URL 133 | $response = Invoke-RestMethod -Uri $url -Method GET -Headers $header ` 134 | -ErrorAction SilentlyContinue 135 | return $response 136 | } 137 | catch [System.Net.WebException] { 138 | if ($_.Exception.Response) { 139 | if ($_.Exception.Response.StatusCode.value__ -eq 404){ 140 | Write-Log "$url does not exist." -warning 141 | return $_.Exception.Response.StatusCode.value__ 142 | } 143 | Write-Log $_.Exception.Response.StatusCode.value__ -error # This is the numeric version. 144 | Write-Log $_.Exception.Response.StatusCode -error # This is a System.Net.HttpStatusCode enum value 145 | } 146 | else { 147 | _PrintError 148 | } 149 | return $false 150 | } 151 | } 152 | elseif ($post_req){ 153 | if(!$body){ 154 | Write-Log "-body parameter is required with -post." 155 | return 156 | } 157 | $content_type = 'application/json' 158 | try { 159 | # POST request against a URL 160 | $response = Invoke-RestMethod -Uri $url -ContentType $content_type ` 161 | -Method POST -Body $body -Headers $header ` 162 | -ErrorAction SilentlyContinue 163 | return $response 164 | } 165 | catch [System.Net.WebException] { 166 | $response = $_.Exception.Response 167 | if ($response) { 168 | if ($response.StatusCode.value__ -eq 409){ 169 | Write-Log "$path already exists." -warning 170 | return $response.StatusCode.value__ 171 | } 172 | Write-Log "Failed to POST: $path" 173 | Write-Log $response.StatusCode -error # This is a System.Net.HttpStatusCode enum value 174 | Write-Log $response.StatusCode.value__ -error # This is the numeric version. 175 | } 176 | else { 177 | _PrintError 178 | } 179 | return $false 180 | } 181 | catch { 182 | _PrintError 183 | Write-Log $_.Exception.GetType().FullName -error 184 | Write-Log $_.Exception -error 185 | return $false 186 | } 187 | } 188 | } 189 | 190 | function CreateRunTimeVariable { 191 | <# 192 | .SYNOPSIS 193 | Create a new runtime variable during run time. 194 | .DESCRIPTION 195 | Generate new run time variable for the instance 196 | .EXAMPLE 197 | CreateRunTimeVariable -config_path -var_name 198 | #> 199 | param ( 200 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 201 | $config_path, 202 | [Alias('random')] 203 | [Switch] $random_var, 204 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 205 | $var_name, 206 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 207 | $var_text, 208 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 209 | $var_value 210 | ) 211 | 212 | # Get access Token to auth for RunTime 213 | try { 214 | $access_token = (_FetchFromMetaData -property ` 215 | 'service-accounts/default/token'| ` 216 | ConvertFrom-Json).access_token 217 | } 218 | catch { 219 | _PrintError 220 | Write-Log $_.Exception.GetType().FullName -error 221 | Write-Log 'Failed to get access token for Runtime Config' -error 222 | return $false 223 | } 224 | 225 | if ($random_var) { 226 | $rand_num = Get-Random 227 | $var_name += "/$rand_num" 228 | } 229 | 230 | Write-Log "Writing $var_name -> $config_path" 231 | # Generate the body to create the key variable 232 | $variable = @{ 233 | name = "$config_path/variables/$var_name" 234 | } 235 | 236 | $var_json = $variable | ConvertTo-Json 237 | 238 | # POST the request 239 | $response = _RunTimeQuery -post -path "$config_path/variables" ` 240 | -body $var_json -access_token $access_token 241 | if ($response) { 242 | Write-Log "Created: $config_path/variables/$var_name, with response: $response" 243 | return $response 244 | } 245 | else{ 246 | Write-Log "Failed to create RunTimeConfig: $config_path/variables" -error 247 | return $false 248 | } 249 | } 250 | 251 | function CreateSCTask { 252 | <# 253 | .SYNOPSIS 254 | Create a Scheduled Task. 255 | .DESCRIPTION 256 | Generate new scheduledTask Action 257 | .EXAMPLE 258 | CreateSCTask -task_name 259 | #> 260 | param ( 261 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 262 | $name, 263 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 264 | $user, 265 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 266 | $password, 267 | [Alias('file')] 268 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 269 | $sch_file, 270 | [Alias('trigger')] 271 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 272 | $when_to_trigger 273 | ) 274 | 275 | $trigger = $null 276 | Write-Log "Creating task $name" 277 | 278 | # Define trigger 279 | if ($when_to_trigger){ 280 | $trigger = $when_to_trigger 281 | } 282 | else { 283 | $trigger = New-ScheduledTaskTrigger -AtStartup 284 | } 285 | 286 | $action = New-ScheduledTaskAction -Execute "powershell.exe" -argument ` 287 | "-ExecutionPolicy Bypass -NonInteractive -NoProfile -File $sch_file -AsJob" 288 | $setting = New-ScheduledTaskSettingsSet 289 | try { 290 | Write-Log "Adding task: $name, with user: $script:domain_service_account." 291 | Register-ScheduledTask $name -Action $action -Trigger $trigger -Settings $setting ` 292 | -User $user -Password $password -RunLevel Highest 293 | } 294 | catch { 295 | _PrintError 296 | } 297 | } 298 | 299 | function DeleteSCTask { 300 | <# 301 | .SYNOPSIS 302 | Deletes a Scheduled Task. 303 | .DESCRIPTION 304 | Deletes a scheduledTask Action 305 | .EXAMPLE 306 | DeleteSCTask -task_name 307 | #> 308 | param ( 309 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 310 | $name 311 | ) 312 | Write-Log "Unregistering $name" 313 | try { 314 | Unregister-ScheduledTask -TaskName $name -Confirm:$false 315 | } 316 | catch { 317 | _PrintError 318 | } 319 | } 320 | 321 | function WaitForRuntime { 322 | <# 323 | .SYNOPSIS 324 | Waits for a runtime variable 325 | .DESCRIPTION 326 | Waits for a runtime variable before giving up 327 | .EXAMPLE 328 | WaitForRuntime -path $path -timeout 329 | #> 330 | param ( 331 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 332 | $path, 333 | [Alias('timeout')] 334 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 335 | $wait_time_min 336 | ) 337 | # Get RuntimeConfig URL for the deployment 338 | $access_token = (_FetchFromMetaData -property ` 339 | 'service-accounts/default/token'| ` 340 | ConvertFrom-Json).access_token 341 | $runtime_config = _GetRuntimeConfig 342 | $query_path = "$runtime_config/variables/$path" 343 | 344 | Write-Logger "Querying path $query_path" 345 | if ($wait_time_min) { 346 | for ($i=0; $i -le $wait_time_min; $i++) { 347 | $response = _RunTimeQuery -get -path $query_path -access_token $access_token 348 | 349 | if($response) { 350 | if ($response -eq 404) { 351 | Write-Logger "$query_path not available. Will retry after 60 seconds.." 352 | Start-Sleep -s 60 353 | } 354 | else { 355 | Write-Logger "$query_path is available." 356 | return $response 357 | } 358 | } 359 | else { 360 | Write-Logger "Something went wrong" 361 | return $false 362 | } 363 | } 364 | } 365 | else { 366 | return _RunTimeQuery -get -path $query_path -access_token $access_token 367 | } 368 | } 369 | 370 | function Write-Logger { 371 | param ( 372 | [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] 373 | [AllowEmptyString()] 374 | [string]$data, 375 | [string]$port = 'COM1' 376 | ) 377 | 378 | $timestamp = $(Get-Date) 379 | $timestampped_msg = "$timestamp $data" 380 | try { 381 | # define a new object to read serial ports 382 | $serial_port = New-Object System.IO.Ports.SerialPort $port, 9600, None, 8, One 383 | $serial_port.Open() 384 | # Write to the serial port 385 | $serial_port.WriteLine($timestampped_msg) 386 | Write-Host $timestampped_msg 387 | } 388 | catch { 389 | Write-Host 'Error writing to serial port' 390 | continue 391 | } 392 | finally { 393 | if ($serial_port) { 394 | $serial_port.Close() 395 | } 396 | } 397 | } 398 | 399 | function Write-ToReg { 400 | <# 401 | .SYNOPSIS 402 | Write To registry 403 | .DESCRIPTION 404 | Write a key to regisry 405 | .EXAMPLE 406 | Write-ToReg 407 | #> 408 | param ( 409 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 410 | $path 411 | ) 412 | # Create registry key 413 | try { 414 | $result = New-Item -Path $path -Force 415 | return $true 416 | } 417 | catch [System.IO.IOException] { 418 | Write-Log "$_.Exception.Message" 419 | Write-Log $result 420 | return $false 421 | } 422 | } 423 | 424 | function UpdateRunTimeWaiter { 425 | <# 426 | .SYNOPSIS 427 | Updtes a RunTimeWaiter. 428 | .DESCRIPTION 429 | Update RunTimeWaiter POST to a given path. By default writes to success 430 | .EXAMPLE 431 | UpdateRunTimeWaiter 432 | .EXAMPLE 433 | UpdateRunTimeWaiter -failure 434 | #> 435 | param ( 436 | [Switch] $failure, 437 | [Alias('path')] 438 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 439 | $status_var_path 440 | ) 441 | 442 | $key_path = $null 443 | $config_name = _GetRuntimeConfig 444 | 445 | if(!$config_name){ 446 | Write-Log "Could not find runtime_config from metadata server" -error 447 | return $false 448 | } 449 | 450 | if (!$status_var_path) { 451 | $status_var_path = _FetchFromMetaData -property ` 452 | 'attributes/status-variable-path' 453 | } 454 | else { 455 | Write-Log "Writing to custom subpath: $status_var_path" 456 | } 457 | 458 | if ($failure) { 459 | $key_path = "$status_var_path/failure" 460 | } 461 | else { 462 | $key_path = "$status_var_path/success" 463 | } 464 | 465 | $response = CreateRunTimeVariable -config_path $config_name -var_name $key_path -random 466 | } 467 | 468 | # Export all modules. 469 | Export-ModuleMember -Function * -Alias * 470 | 471 | # Clear out any existing errors. 472 | $error.Clear() | Out-Null -------------------------------------------------------------------------------- /powershell/c2d/gce_base.psm1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | <# 18 | .SYNOPSIS 19 | GCE Base Modules. 20 | .DESCRIPTION 21 | Base modules needed for GCE Powershell scripts to run scripts to run. 22 | 23 | #requires -version 3.0 24 | #> 25 | 26 | # Default Values 27 | $global:write_to_serial = $false 28 | $global:metadata_server = 'metadata.google.internal' 29 | $global:hostname = [System.Net.Dns]::GetHostName() 30 | $global:log_file = $null 31 | 32 | # Functions 33 | function _AddToPath { 34 | <# 35 | .SYNOPSIS 36 | Adds GCE tool dir to SYSTEM PATH 37 | .DESCRIPTION 38 | This is a helper function which adds location to path 39 | #> 40 | param ( 41 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 42 | [Alias('path')] 43 | $path_to_add 44 | ) 45 | 46 | # Check if folder exists on the file system. 47 | if (!(Test-Path $path_to_add)) { 48 | Write-Log "$path_to_add does not exist, cannot be added to $env:PATH." 49 | return 50 | } 51 | 52 | try { 53 | $path_reg_key = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' 54 | $current_path = (Get-ItemProperty $path_reg_key).Path 55 | $check_path = ($current_path).split(';') | ? {$_ -like $path_to_add} 56 | } 57 | catch { 58 | Write-Log 'Could not read path from the registry.' 59 | _PrintError 60 | } 61 | # See if the folder is already in the path. 62 | if ($check_path) { 63 | Write-Log 'Folder already in system path.' 64 | } 65 | else { 66 | try { 67 | Write-Log "Adding $path_to_add to SYSTEM path." 68 | $new_path = $current_path + ';' + $path_to_add 69 | $env:Path = $new_path 70 | Set-ItemProperty $path_reg_key -name 'Path' -value $new_path 71 | } 72 | catch { 73 | Write-Log 'Failed to add to SYSTEM path.' 74 | _PrintError 75 | } 76 | } 77 | } 78 | 79 | 80 | function Clear-EventLogs { 81 | <# 82 | .SYNOPSIS 83 | Clear all eventlog enteries. 84 | .DESCRIPTION 85 | This uses the Get-Eventlog and Clear-EventLog powershell functions to 86 | clean the eventlogs for a machine. 87 | #> 88 | 89 | Write-Log 'Clearing events in EventViewer.' 90 | Get-WinEvent -ListLog * | 91 | Where-Object {($_.IsEnabled -eq 'True') -and ($_.RecordCount -gt 0)} | 92 | ForEach-Object { 93 | try{[System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.ClearLog($_.LogName)}catch{} 94 | } 95 | } 96 | 97 | 98 | function Clear-TempFolders { 99 | <# 100 | .SYNOPSIS 101 | Delete all files from temp folder location. 102 | .DESCRIPTION 103 | This function calls an array variable which contain location of all the 104 | temp files and folder which needs to be cleared out. We use the 105 | Remove-Item routine to delete the files in the temp directories. 106 | #> 107 | 108 | # Array of files and folder that need to be deleted. 109 | @("C:\Windows\Temp\*", "C:\Windows\Prefetch\*", 110 | "C:\Documents and Settings\*\Local Settings\temp\*\*", 111 | "C:\Users\*\Appdata\Local\Temp\*\*", 112 | "C:\Users\*\Appdata\Local\Microsoft\Internet Explorer\*", 113 | "C:\Users\*\Appdata\LocalLow\Temp\*\*", 114 | "C:\Users\*\Appdata\LocalLow\Microsoft\Internet Explorer\*") | ForEach-Object { 115 | if (Test-Path $_) { 116 | Remove-Item $_ -recurse -force -ErrorAction SilentlyContinue 117 | } 118 | } 119 | } 120 | 121 | 122 | function Get-MetaData { 123 | <# 124 | .SYNOPSIS 125 | Get attributes from GCE instances metadata. 126 | .DESCRIPTION 127 | Use Net.WebClient to fetch data from metadata server. 128 | .PARAMETER property 129 | Name of instance metadata property we want to fetch. 130 | .PARAMETER filename 131 | Name of file to save metadata contents to. If left out, returns contents. 132 | .EXAMPLE 133 | $hostname = _FetchFromMetaData -property 'hostname' 134 | Get-MetaData -property 'startup-script' -file 'script.bat' 135 | #> 136 | param ( 137 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 138 | $property, 139 | $filename = $null, 140 | [switch] $project_only = $false, 141 | [switch] $instance_only = $false 142 | ) 143 | 144 | $request_url = '/computeMetadata/v1/instance/' 145 | if ($project_only) { 146 | $request_url = '/computeMetadata/v1/project/' 147 | } 148 | 149 | $url = "http://$global:metadata_server$request_url$property" 150 | 151 | try { 152 | $client = _GetWebClient 153 | #Header 154 | $client.Headers.Add('Metadata-Flavor', 'Google') 155 | # Get Data 156 | if ($filename) { 157 | $client.DownloadFile($url, $filename) 158 | return 159 | } 160 | else { 161 | return ($client.DownloadString($url)).Trim() 162 | } 163 | } 164 | catch [System.Net.WebException] { 165 | if ($project_only -or $instance_only) { 166 | Write-Log "$property value is not set or metadata server is not reachable." 167 | } 168 | else { 169 | return (_FetchFromMetaData -project_only -property $property -filename $filename) 170 | } 171 | } 172 | catch { 173 | Write-Log "Unknown error in reading $url." 174 | _PrintError 175 | } 176 | } 177 | 178 | 179 | function _GenerateRandomPassword { 180 | <# 181 | .SYNOPSIS 182 | Generates random password which meet windows complexity requirements. 183 | .DESCRIPTION 184 | This function generates a password to be set on built-in account before 185 | it is disabled. 186 | .OUTPUTS 187 | Returns String 188 | .EXAMPLE 189 | _GeneratePassword 190 | #> 191 | 192 | # Define length of the password. Maximum and minimum. 193 | [int] $pass_min = 20 194 | [int] $pass_max = 35 195 | [string] $random_password = $null 196 | 197 | # Random password length should help prevent masking attacks. 198 | $password_length = Get-Random -Minimum $pass_min -Maximum $pass_max 199 | 200 | # Choose a set of ASCII characters we'll use to generate new passwords from. 201 | $ascii_char_set = $null 202 | for ($x=33; $x -le 126; $x++) { 203 | $ascii_char_set+=,[char][byte]$x 204 | } 205 | 206 | # Generate random set of characters. 207 | for ($loop=1; $loop -le $password_length; $loop++) { 208 | $random_password += ($ascii_char_set | Get-Random) 209 | } 210 | return $random_password 211 | } 212 | 213 | 214 | function _GetCOMPorts { 215 | <# 216 | .SYNOPSIS 217 | Get available serial ports. Check if a port exists, if yes returns $true 218 | .DESCRIPTION 219 | This function is used to check if a port exists on this machine. 220 | .PARAMETER $portname 221 | Name of the port you want to check if it exists. 222 | .OUTPUTS 223 | [boolean] 224 | .EXAMPLE 225 | _GetCOMPorts 226 | #> 227 | 228 | param ( 229 | [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 230 | [String]$portname 231 | ) 232 | 233 | $exists = $false 234 | try { 235 | # Read available COM ports. 236 | $com_ports = [System.IO.Ports.SerialPort]::getportnames() 237 | if ($com_ports -match $portname) { 238 | $exists = $true 239 | } 240 | } 241 | catch { 242 | _PrintError 243 | } 244 | return $exists 245 | } 246 | 247 | 248 | function _GetWebClient { 249 | <# 250 | .SYNOPSIS 251 | Get Net.WebClient object. 252 | .DESCRIPTION 253 | Generata Webclient object for clients to use. 254 | .EXAMPLE 255 | $hostname = _GetWebClient 256 | #> 257 | $client = $null 258 | try { 259 | # WebClient to return. 260 | $client = New-Object Net.WebClient 261 | } 262 | catch [System.Net.WebException] { 263 | Write-Log 'Could not generate a WebClient object.' 264 | _PrintError 265 | } 266 | return $client 267 | } 268 | 269 | 270 | function _PrintError { 271 | <# 272 | .SYNOPSIS 273 | Prints Error Messages 274 | .DESCRIPTION 275 | This is a helper function which prints out error messages in catch 276 | .OUTPUTS 277 | Error message found during execution is printed out to the console. 278 | .EXAMPLE 279 | _PrintError 280 | #> 281 | 282 | # See all error objects. 283 | $error_obj = Get-Variable -Name Error -Scope 2 -ErrorAction SilentlyContinue 284 | if ($error_obj) { 285 | try { 286 | $message = $($error_obj.Value.Exception[0].Message) 287 | $line_no = $($error_obj.Value.InvocationInfo[0].ScriptLineNumber) 288 | $line_info = $($error_obj.Value.InvocationInfo[0].Line) 289 | $hresult = $($error_obj.Value.Exception[0].HResult) 290 | $calling_script = $($error_obj.Value.InvocationInfo[0].ScriptName) 291 | 292 | # Format error string 293 | if ($error_obj.Value.Exception[0].InnerException) { 294 | $inner_msg = $error_obj.Value.Exception[0].InnerException.Message 295 | $errmsg = "$inner_msg : $message {Line: $line_no : $line_info, HResult: $hresult, Script: $calling_script}" 296 | } 297 | else { 298 | $errmsg = "$message {Line: $line_no : $line_info, HResult: $hresult, Script: $calling_script}" 299 | } 300 | # Write message to output. 301 | Write-Log $errmsg -error 302 | } 303 | catch { 304 | Write-Log $_.Exception.GetBaseException().Message -error 305 | } 306 | } 307 | 308 | # Clear out the error. 309 | $error.Clear() | Out-Null 310 | } 311 | 312 | 313 | function Invoke-ExternalCommand { 314 | <# 315 | .SYNOPSIS 316 | Run External Command. 317 | .DESCRIPTION 318 | This function calls an external command outside of the powershell script and logs the output. 319 | .PARAMETER Executable 320 | Executable that needs to be run. 321 | .PARAMETER Arguments 322 | Arguments for the executable. Default is NULL. 323 | .EXAMPLE 324 | Invoke-ExternalCommand dir c:\ 325 | #> 326 | [CmdletBinding(SupportsShouldProcess=$true)] 327 | param ( 328 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 329 | [string]$Executable, 330 | [Parameter(ValueFromRemainingArguments=$true, 331 | ValueFromPipelineByPropertyName=$true)] 332 | $Arguments = $null 333 | ) 334 | Write-Log "Running '$Executable' with arguments '$Arguments'" 335 | $out = &$Executable $Arguments 2>&1 | Out-String 336 | if ($out.Trim()) { 337 | $out.Trim().Split("`n") | ForEach-Object { 338 | Write-Log "--> $_" 339 | } 340 | } 341 | } 342 | 343 | 344 | function _TestAdmin { 345 | <# 346 | .SYNOPSIS 347 | Checks if the current Powershell instance is running with 348 | elevated privileges or not. 349 | .OUTPUTS 350 | System.Boolean 351 | True if the current Powershell is elevated, false if not. 352 | #> 353 | try { 354 | $identity = [Security.Principal.WindowsIdentity]::GetCurrent() 355 | $principal = New-Object Security.Principal.WindowsPrincipal -ArgumentList $identity 356 | return $principal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) 357 | } 358 | catch { 359 | Write-Log 'Failed to determine if the current user has elevated privileges.' 360 | _PrintError 361 | } 362 | } 363 | 364 | 365 | function _TestTCPPort { 366 | <# 367 | .SYNOPSIS 368 | Test TCP port on remote server 369 | .DESCRIPTION 370 | Use .Net Socket connection to connect to remote host and check if port is 371 | open. 372 | .PARAMETER remote_host 373 | Remote host you want to check TCP port for. 374 | .PARAMETER port_number 375 | TCP port number you want to check. 376 | .PARAMETER timeout 377 | Time you want to wait for. 378 | .RETURNS 379 | Return bool. $true if server is reachable at tcp port $false is not. 380 | .EXAMPLE 381 | _TestTCPPort -host 127.0.0.1 -port 80 382 | #> 383 | param ( 384 | [Alias('host')] 385 | [string]$remote_host, 386 | [Alias('port')] 387 | [int]$port_number, 388 | [int]$timeout = 3000 389 | ) 390 | 391 | $status = $false 392 | try { 393 | # Create a TCP Client. 394 | $socket = New-Object Net.Sockets.TcpClient 395 | # Use the TCP Client to connect to remote host port. 396 | $connection = $socket.BeginConnect($remote_host, $port_number, $null, $null) 397 | # Set the wait time 398 | $wait = $connection.AsyncWaitHandle.WaitOne($timeout, $false) 399 | if (!$wait) { 400 | # Connection failed, timeout reached. 401 | $socket.Close() 402 | } 403 | else { 404 | # Close the connection and report the error if there is one. 405 | $socket.EndConnect($connection) | Out-Null 406 | if (!$?) { 407 | Write-Log $error[0] 408 | } 409 | else { 410 | $status = $true 411 | } 412 | $socket.Close() 413 | } 414 | } 415 | catch { 416 | _PrintError 417 | } 418 | return $status 419 | } 420 | 421 | 422 | function Write-SerialPort { 423 | <# 424 | .SYNOPSIS 425 | Sending data to serial port. 426 | .DESCRIPTION 427 | Use this function to send data to serial port. 428 | .PARAMETER portname 429 | Name of port. The port to use (for example, COM1). 430 | .PARAMETER baud_rate 431 | The baud rate. 432 | .PARAMETER parity 433 | Specifies the parity bit for a SerialPort object. 434 | None: No parity check occurs (default). 435 | Odd: Sets the parity bit so that the count of bits set is an odd number. 436 | Even: Sets the parity bit so that the count of bits set is an even number. 437 | Mark: Leaves the parity bit set to 1. 438 | Space: Leaves the parity bit set to 0. 439 | .PARAMETER data_bits 440 | The data bits value. 441 | .PARAMETER stop_bits 442 | Specifies the number of stop bits used on the SerialPort object. 443 | None: No stop bits are used. This value is Currently not supported by the 444 | stop_bits. 445 | One: One stop bit is used (default). 446 | Two: Two stop bits are used. 447 | OnePointFive: 1.5 stop bits are used. 448 | .PARAMETER data 449 | Data to be sent to serial port. 450 | .PARAMETER wait_for_respond 451 | Wait for result of data sent. 452 | .PARAMETER close 453 | Remote close connection. 454 | .EXAMPLE 455 | Send data to serial port and exit. 456 | Write-SerialPort -portname COM1 -data 'Hello World' 457 | .EXAMPLE 458 | Send data to serial port and wait for respond. 459 | Write-SerialPort -portname COM1 -data 'dir C:\' -wait_for_respond 460 | #> 461 | [CmdletBinding(supportsshouldprocess=$true)] 462 | param ( 463 | [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 464 | [string]$portname, 465 | [Int]$baud_rate = 9600, 466 | [ValidateSet('None', 'Odd', 'Even', 'Mark', 'Space')] 467 | [string]$parity = 'None', 468 | [int]$data_bits = 8, 469 | [ValidateSet('None', 'One', 'Even', 'Two', 'OnePointFive')] 470 | [string]$stop_bits = 'One', 471 | [string]$data, 472 | [Switch]$wait_for_respond, 473 | [Switch]$close 474 | ) 475 | 476 | if ($psCmdlet.shouldProcess($portname , 'Write data to local serial port')) { 477 | if ($close) { 478 | $data = 'close' 479 | $wait_for_respond = $false 480 | } 481 | try { 482 | # Define a new object to read serial ports. 483 | $port = New-Object System.IO.Ports.SerialPort $portname, $baud_rate, ` 484 | $parity, $data_bits, $stop_bits 485 | $port.Open() 486 | # Write to the serial port. 487 | $port.WriteLine($data) 488 | # If wait_for_resond is specified. 489 | if ($wait_for_respond) { 490 | $result = $port.ReadLine() 491 | $result.Replace("#^#","`n") 492 | } 493 | $port.Close() 494 | } 495 | catch { 496 | _PrintError 497 | } 498 | } 499 | } 500 | 501 | 502 | function Write-Log { 503 | <# 504 | .SYNOPSIS 505 | Generate Log for the script. 506 | .DESCRIPTION 507 | Generate log messages, if COM1 port found write output to COM1 also. 508 | .PARAMETER $msg 509 | Message that needs to be logged 510 | .PARAMETER $is_important 511 | Surround the message with a line of hyphens. 512 | .PARAMETER $is_error 513 | Mark messages as Error in red text. 514 | .PARAMETER $is_warning 515 | Mark messages as Warning in yellow text. 516 | #> 517 | param ( 518 | [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 519 | [String]$msg, 520 | [Alias('important')] 521 | [Switch] $is_important, 522 | [Alias('error')] 523 | [Switch] $is_error, 524 | [Alias('warning')] 525 | [Switch] $is_warning 526 | ) 527 | $timestamp = $(Get-Date -Format 'yyyy/MM/dd HH:mm:ss') 528 | if (-not ($global:logger)) { 529 | $global:logger = '' 530 | } 531 | try { 532 | # Add a boundary around an important message. 533 | if ($is_important) { 534 | $boundary = '-' * 60 535 | $timestampped_msg = @" 536 | ${timestamp} ${global:logger}: ${boundary} 537 | ${timestamp} ${global:logger}: ${msg} 538 | ${timestamp} ${global:logger}: ${boundary} 539 | "@ 540 | } 541 | else { 542 | $timestampped_msg = "${timestamp} ${global:logger}: ${msg}" 543 | } 544 | # If a log file is set, use it. 545 | if ($global:log_file) { 546 | Add-Content $global:log_file "$timestampped_msg" 547 | } 548 | # If COM1 exists write msg to console. 549 | if ($global:write_to_serial) { 550 | Write-SerialPort -portname 'COM1' -data "$timestampped_msg" -ErrorAction SilentlyContinue 551 | } 552 | if ($is_error) { 553 | Write-Host "$timestampped_msg" -foregroundcolor red 554 | } 555 | elseif ($is_warning) { 556 | Write-Host "$timestampped_msg" -foregroundcolor yellow 557 | } 558 | else { 559 | Write-Host "$timestampped_msg" 560 | } 561 | } 562 | catch { 563 | _PrintError 564 | continue 565 | } 566 | } 567 | 568 | 569 | function Set-LogFile { 570 | param ( 571 | [parameter(Position=0, Mandatory=$true)] 572 | [String]$filename 573 | ) 574 | Write-Log "Initializing log file $filename." 575 | if (Test-Path $filename) { 576 | Write-Log 'Log file already exists.' 577 | $global:log_file = $filename 578 | } 579 | else { 580 | try { 581 | Write-Log 'Creating log file.' 582 | New-Item $filename -Type File -ErrorAction Stop 583 | $global:log_file = $filename 584 | } 585 | catch { 586 | _PrintError 587 | } 588 | } 589 | Write-Log "Log file set to $global:log_file" 590 | } 591 | 592 | 593 | # Export all modules. 594 | New-Alias -Name _WriteToSerialPort -Value Write-SerialPort 595 | New-Alias -Name _RunExternalCMD -Value Invoke-ExternalCommand 596 | New-Alias -Name _ClearEventLogs -Value Clear-EventLogs 597 | New-Alias -Name _ClearTempFolders -Value Clear-TempFolders 598 | New-Alias -Name _FetchFromMetadata -Value Get-Metadata 599 | Export-ModuleMember -Function * -Alias * 600 | 601 | if (_GetCOMPorts -portname 'COM1') { 602 | $global:write_to_serial = $true 603 | } 604 | 605 | # Clear out any existing errors. 606 | $error.Clear() | Out-Null 607 | 608 | # SIG # Begin signature block 609 | # MIIXsQYJKoZIhvcNAQcCoIIXojCCF54CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 610 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 611 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUFYCbOohI0jAoGr8qlHX8EbQb 612 | # OI6gghLXMIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B 613 | # AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG 614 | # A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh 615 | # d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg 616 | # Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV 617 | # UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu 618 | # dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN 619 | # AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q 620 | # WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC 621 | # i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4 622 | # ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3 623 | # +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI 624 | # fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd 625 | # BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG 626 | # CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB 627 | # Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro 628 | # YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV 629 | # HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y 630 | # MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf 631 | # plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y 632 | # 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq 633 | # IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3 634 | # DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh 635 | # dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD 636 | # QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE 637 | # BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT 638 | # eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN 639 | # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow 640 | # mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0 641 | # jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu 642 | # ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh 643 | # d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz 644 | # C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB 645 | # o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO 646 | # BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw 647 | # Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90 648 | # cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx 649 | # oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy 650 | # bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV 651 | # HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa 652 | # 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH 653 | # bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73 654 | # BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR 655 | # EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW 656 | # yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu 657 | # e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw 658 | # ggTdMIIDxaADAgECAhAqnCGsqqY6PFinuTIr7pSNMA0GCSqGSIb3DQEBCwUAMH8x 659 | # CzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3JhdGlvbjEfMB0G 660 | # A1UECxMWU3ltYW50ZWMgVHJ1c3QgTmV0d29yazEwMC4GA1UEAxMnU3ltYW50ZWMg 661 | # Q2xhc3MgMyBTSEEyNTYgQ29kZSBTaWduaW5nIENBMB4XDTE1MTIxNjAwMDAwMFoX 662 | # DTE4MTIxNjIzNTk1OVowZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju 663 | # aWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2dsZSBJbmMx 664 | # EzARBgNVBAMMCkdvb2dsZSBJbmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 665 | # AoIBAQDEDYLEQSko5f0MP6XHDma9pcSLs4qshAOfhC443waxTv0zYFg4Nt0iz9/x 666 | # UB9H8VUFwYEB5yg+/1+JEgnq36oXSSxxq0jRnS70UeAD4PcWbHsMInVtfh9JxEMo 667 | # iEHcbO0TKgOZ62IU+TUmbhIsA+L3gbkaBWcGfKYaW+0gFeUtg96ONvoeCEEcGkif 668 | # tvHDLwITS6fKuu8cWG+O0w8UpAsrXbr0WqMNZDSlitePTSJmTaSu4fnNxljmxhF3 669 | # Mt+63zlIitEn1zN3qMnkXu36Es/z/fruq4CGEzTrWn5vbBvu2EuyzHeYh6zK9btk 670 | # b0keW5FjUB9jLYMncwefKxb0e3EpAgMBAAGjggFuMIIBajAJBgNVHRMEAjAAMA4G 671 | # A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzBmBgNVHSAEXzBdMFsG 672 | # C2CGSAGG+EUBBxcDMEwwIwYIKwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20v 673 | # Y3BzMCUGCCsGAQUFBwICMBkaF2h0dHBzOi8vZC5zeW1jYi5jb20vcnBhMB8GA1Ud 674 | # IwQYMBaAFJY7U/B5M5evfYPvLivMyreGHnJmMCsGA1UdHwQkMCIwIKAeoByGGmh0 675 | # dHA6Ly9zdi5zeW1jYi5jb20vc3YuY3JsMFcGCCsGAQUFBwEBBEswSTAfBggrBgEF 676 | # BQcwAYYTaHR0cDovL3N2LnN5bWNkLmNvbTAmBggrBgEFBQcwAoYaaHR0cDovL3N2 677 | # LnN5bWNiLmNvbS9zdi5jcnQwEQYJYIZIAYb4QgEBBAQDAgQQMBYGCisGAQQBgjcC 678 | # ARsECDAGAQEAAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAj55OTr9uoTa+vVOjYJpWA 679 | # zSORcO0LW7Hp2N0eQDd4lxjtn+WEZ4UGULXxq+aDWhd7Ub5/GMZHXiuq9KAfNT4F 680 | # n0NA95/R9OGnAvOOyXH+GDdIQtfkNnMQktTY2RzEJlgYZ7YkImljAvdJUWt19rR9 681 | # Vv8s9Ij3Z28IhvOLCzACf22S2U69mfd7dIYMy7mtLL9EeagAgpxi9KoR39K/8OGS 682 | # KBGQu14ziIaWTd0Lr8NnoZUtRDLG+ve4gMFOOL4ftoT38SExZ0mon4p1B987OsPq 683 | # cs1Af6fafMkufKkM8V1cgkJiuUmUj3DmpcBfF/tANsE6iWMDHD9moD2PoUxOXKy/ 684 | # MIIFWTCCBEGgAwIBAgIQPXjX+XZJYLJhffTwHsqGKjANBgkqhkiG9w0BAQsFADCB 685 | # yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL 686 | # ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp 687 | # U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW 688 | # ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 689 | # aG9yaXR5IC0gRzUwHhcNMTMxMjEwMDAwMDAwWhcNMjMxMjA5MjM1OTU5WjB/MQsw 690 | # CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV 691 | # BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVjIENs 692 | # YXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQAD 693 | # ggEPADCCAQoCggEBAJeDHgAWryyx0gjE12iTUWAecfbiR7TbWE0jYmq0v1obUfej 694 | # DRh3aLvYNqsvIVDanvPnXydOC8KXyAlwk6naXA1OpA2RoLTsFM6RclQuzqPbROlS 695 | # Gz9BPMpK5KrA6DmrU8wh0MzPf5vmwsxYaoIV7j02zxzFlwckjvF7vjEtPW7ctZlC 696 | # n0thlV8ccO4XfduL5WGJeMdoG68ReBqYrsRVR1PZszLWoQ5GQMWXkorRU6eZW4U1 697 | # V9Pqk2JhIArHMHckEU1ig7a6e2iCMe5lyt/51Y2yNdyMK29qclxghJzyDJRewFZS 698 | # AEjM0/ilfd4v1xPkOKiE1Ua4E4bCG53qWjjdm9sCAwEAAaOCAYMwggF/MC8GCCsG 699 | # AQUFBwEBBCMwITAfBggrBgEFBQcwAYYTaHR0cDovL3MyLnN5bWNiLmNvbTASBgNV 700 | # HRMBAf8ECDAGAQH/AgEAMGwGA1UdIARlMGMwYQYLYIZIAYb4RQEHFwMwUjAmBggr 701 | # BgEFBQcCARYaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9jcHMwKAYIKwYBBQUHAgIw 702 | # HBoaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9ycGEwMAYDVR0fBCkwJzAloCOgIYYf 703 | # aHR0cDovL3MxLnN5bWNiLmNvbS9wY2EzLWc1LmNybDAdBgNVHSUEFjAUBggrBgEF 704 | # BQcDAgYIKwYBBQUHAwMwDgYDVR0PAQH/BAQDAgEGMCkGA1UdEQQiMCCkHjAcMRow 705 | # GAYDVQQDExFTeW1hbnRlY1BLSS0xLTU2NzAdBgNVHQ4EFgQUljtT8Hkzl699g+8u 706 | # K8zKt4YecmYwHwYDVR0jBBgwFoAUf9Nlp8Ld7LvwMAnzQzn6Aq8zMTMwDQYJKoZI 707 | # hvcNAQELBQADggEBABOFGh5pqTf3oL2kr34dYVP+nYxeDKZ1HngXI9397BoDVTn7 708 | # cZXHZVqnjjDSRFph23Bv2iEFwi5zuknx0ZP+XcnNXgPgiZ4/dB7X9ziLqdbPuzUv 709 | # M1ioklbRyE07guZ5hBb8KLCxR/Mdoj7uh9mmf6RWpT+thC4p3ny8qKqjPQQB6rqT 710 | # og5QIikXTIfkOhFf1qQliZsFay+0yQFMJ3sLrBkFIqBgFT/ayftNTI/7cmd3/SeU 711 | # x7o1DohJ/o39KK9KEr0Ns5cF3kQMFfo2KwPcwVAB8aERXRTl4r0nS1S+K4ReD6bD 712 | # dAUK75fDiSKxH3fzvc1D1PFMqT+1i4SvZPLQFCExggREMIIEQAIBATCBkzB/MQsw 713 | # CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV 714 | # BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVjIENs 715 | # YXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQQIQKpwhrKqmOjxYp7kyK+6UjTAJ 716 | # BgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0B 717 | # CQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAj 718 | # BgkqhkiG9w0BCQQxFgQUEeKZi2AXA3etQbuOeFvREFxZ4nIwDQYJKoZIhvcNAQEB 719 | # BQAEggEAV4qbSXmlMNe8uP5tkPpfES6lxflrSHalr1+lEh9wfXrxR7LKvhdaOblM 720 | # rTxxQJBe6RGU3Ag86xWcByCQsUmHekCs2x1lTR/g3xFRWqpKvu4HIiB8iykZMgmu 721 | # aOvWBLBAxJ2gci1DELF+fTiBfmy8Jcz1UX0OOOwkgsvLbBVlU5SPZ150e0DdSfjh 722 | # nt3MVCBKDUNQ99hKDaWvqjdj/VX34/ExuEOBroxflTVWM21jMkRQ8aIEz940coGi 723 | # oB0l6GfGQy/VMsWgU7W4IEPpXH6gmI9EIwuzN+eim9N5xwGHqI/s/LzFpneX8pCX 724 | # 7jsAe9SYrWYU5LEoo0SmS/YcN4ymcqGCAgswggIHBgkqhkiG9w0BCQYxggH4MIIB 725 | # 9AIBATByMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh 726 | # dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD 727 | # QSAtIEcyAhAOz/Q4yP6/NW4E2GqYGxpQMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0B 728 | # CQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xODA3MTgxNjQ2MzRaMCMG 729 | # CSqGSIb3DQEJBDEWBBTDikzGBpDxGcjAeJBYtTj2DhEVbDANBgkqhkiG9w0BAQEF 730 | # AASCAQCLISKydxm1Pdur6eSZVdFTmG2+ift7J11+8fc10/TJr2VXv0tlrVgwadkw 731 | # hyp0ceXleVUXZ9sqw2D734jRPottGQKiQkQqBtTf8qF0NqpTXbhA7aBjFCC+wZXP 732 | # 0NkzGI9DLcLe6q+l2Mo7MY96jXID3bTjIBI52Mp1zEWDWhkeFjPCDdHWw03X4g0X 733 | # TXisAW/mHmMHYdXVXvDrY2ym9wyz4DIO4pWCSIdCA/4FT/G7yY0ba1H5N1hiLBd6 734 | # 9wl922GdZx00p7clwF2OZCH1jLTBfKaSd1NcUonG8oeoQiR2Lm3qoeMnc2uLdw0j 735 | # fwsDpip2ZPs2e7ws+jxs1UoNNc3g 736 | # SIG # End signature block 737 | -------------------------------------------------------------------------------- /powershell/c2d/initial_win_startup_script.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Set-StrictMode -Version Latest 18 | 19 | $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine\sysprep' 20 | 21 | # Import Modules 22 | try { 23 | Import-Module $script:gce_install_dir\gce_base.psm1 -ErrorAction Stop 24 | } 25 | catch [System.Management.Automation.ActionPreferenceStopException] { 26 | Write-Host $_.Exception.GetBaseException().Message 27 | Write-Host ("Unable to import GCE module from $script:gce_install_dir. " + 28 | 'Check error message, or ensure module is present.') 29 | exit 2 30 | } 31 | 32 | # Default Values 33 | $Script:c2d_scripts_bucket = 'c2d-windows/scripts' 34 | $Script:install_path="C:\C2D" # Folder for downloads 35 | $script:show_msgs = $false 36 | $script:write_to_serial = $false 37 | 38 | 39 | # Functions 40 | function DownloadScript { 41 | <# 42 | .SYNOPSIS 43 | Downloads a script to the localmachine from GCS. 44 | .DESCRIPTION 45 | Uses WebClient to download a script file. 46 | .EXAMPLE 47 | DownloadScript -path bucket/.. -filename 48 | #> 49 | param ( 50 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 51 | $path, 52 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 53 | $filename, 54 | [Switch] $overwrite 55 | ) 56 | $storage_url = 'http://storage.googleapis.com' 57 | $download_url = "$storage_url/$path" 58 | 59 | 60 | # Check if file already exists and act accordingly. 61 | if ((Test-path -path $filename)){ 62 | if ($overwrite){ 63 | Write-Log "$filename already exists. Overwrite flag set." 64 | _DeleteFiles -files $filename 65 | } 66 | else { 67 | Write-Log "$filename already exists. Overwrite flag notset." 68 | return $true 69 | } 70 | } 71 | # Download the file 72 | Write-Log "Original download url: $download_url" 73 | # To avoid cache issues 74 | $url = $download_url + "?random=" + (Get-Random).ToString() 75 | 76 | Write-Log "Downloading $url to $filename" 77 | try { 78 | Invoke-WebRequest -Uri $url -OutFile $filename -Headers @{"Cache-Control"="private"} 79 | } 80 | catch [System.Net.WebException] { 81 | $response = $_.Exception.Response 82 | if ($response) { 83 | _PrintError 84 | Write-Log $response.StatusCode -error # This is a System.Net.HttpStatusCode enum value 85 | Write-Log $response.StatusCode.value__ -error # This is the numeric version. 86 | } 87 | else { 88 | $type = $_.Exception.GetType().FullName 89 | $message = $_.Exception.Message 90 | Write-Log "$type $message" 91 | } 92 | return $false 93 | } 94 | 95 | # Check if download successfull 96 | if ((Test-path -path $filename)){ 97 | return $true 98 | } 99 | else { 100 | Write-Log "File not found." 101 | return $false 102 | } 103 | } 104 | 105 | 106 | ## Main 107 | # Instance specific variables 108 | $script_name = 'sql_install.ps1' 109 | $script_subpath = 'sqlserver' 110 | $task_name = "SQLInstall" 111 | 112 | # Create the C:\C2D folder 113 | if (!(Test-path -path $Script:install_path )) { 114 | try { 115 | New-Item -ItemType directory -Path $Script:install_path 116 | } 117 | catch { 118 | _PrintError 119 | exit 1 120 | } 121 | } 122 | 123 | # Download the scripts 124 | # Base Script 125 | $base_script_path = "$Script:c2d_scripts_bucket/c2d_base.psm1" 126 | $base_script = "$Script:install_path\c2d_base.psm1" 127 | if (DownloadScript -path $base_script_path -filename $base_script) { 128 | Write-Log "File downloaded successfully." 129 | } 130 | else { 131 | Write-Log "File not found." 132 | exit 2 133 | } 134 | # Run Script 135 | $run_script = "$Script:install_path\$script_name" 136 | $run_script_path = "$Script:c2d_scripts_bucket/$script_subpath/$script_name" 137 | if (DownloadScript -path $run_script_path -filename $run_script) { 138 | Write-Log "File downloaded successfully." 139 | } 140 | else { 141 | Write-Log "File not found." 142 | exit 2 143 | } 144 | 145 | 146 | # Execute the script 147 | Write-Log "Checking if $task_name sctask exists?" 148 | $sc_task = Get-ScheduledTask -TaskName $task_name -ErrorAction SilentlyContinue 149 | if ($sc_task) { 150 | Write-Log "$task_name schtask exists." 151 | try { 152 | Write-Log "-- Executing sctask $task_name. --" 153 | $response = Start-ScheduledTask -TaskName $task_name 154 | Write-Log $response 155 | 156 | } 157 | catch { 158 | $type = $_.Exception.GetType().FullName 159 | $message = $_.Exception.Message 160 | Write-Log "$type $message" 161 | exit 1 162 | } 163 | } 164 | else { 165 | Write-Log "schtask $task_name does not exists." 166 | Write-Log "Executing: $run_script" 167 | try { 168 | & $run_script -task_name $task_name 169 | } 170 | catch { 171 | $type = $_.Exception.GetType().FullName 172 | $message = $_.Exception.Message 173 | Write-Log "$type $message" 174 | exit 1 175 | } 176 | } -------------------------------------------------------------------------------- /powershell/c2d/sql_install.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | <# 18 | .SYNOPSIS 19 | SQL Server Configuration 20 | 21 | .DESCRIPTION 22 | This Script bootstrap SQL Server Configuration 23 | .EXAMPLE 24 | sql_bootstrap.ps1 25 | .EXAMPLE 26 | sql_bootstrap.ps1 -name 27 | 28 | #requires -version 3.0 29 | #> 30 | [CmdletBinding()] 31 | param ( 32 | [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)] 33 | $task_name=$false, 34 | [Alias('AsJob')] 35 | [Switch] $as_job 36 | ) 37 | 38 | Set-StrictMode -Version Latest 39 | 40 | $script:c2d_dir = 'C:\C2D' 41 | 42 | # Import Modules 43 | try { 44 | Import-Module $script:c2d_dir\c2d_base.psm1 -ErrorAction Stop 45 | } 46 | catch [System.Management.Automation.ActionPreferenceStopException] { 47 | Write-Host $_.Exception.GetBaseException().Message 48 | Write-Host ("Unable to import GCE module from $script:c2d_dir. " + 49 | 'Check error message, or ensure module is present.') 50 | exit 2 51 | } 52 | 53 | # Default Values 54 | $Script:all_nodes = @() 55 | $Script:all_nodes_csv = $null 56 | $Script:all_nodes_fqdn = @() 57 | $Script:backup_key_path = 'backup' 58 | $Script:cluster_name = 'cluster-dbclus' 59 | $Script:cluster_key_path = 'cluster' 60 | $Script:cred_obj = $null 61 | $Script:db_name = 'TestDB' 62 | $Script:db_folder_data = 'SQLData' 63 | $Script:db_folder_log = 'SQLLog' 64 | $Script:db_folder_backup = 'SQLBackup' 65 | $Script:initdb_key_path = 'initdb' 66 | $Script:domain_bios_name = $null 67 | $Script:domain_name = $null 68 | $script:domain_service_account = $null 69 | $script:name_ag = 'cluster-ag' # Name of the SQLServer Availability Group 70 | $script:name_ag_listener= 'cluster-listener' 71 | $script:node2 = $null 72 | $Script:replica_key_path = 'replica' 73 | $script:remote_nodes = @() 74 | $script:sa_password = $null 75 | $Script:static_ip = @() 76 | $Script:static_listner_ip = @() 77 | $Script:service_account = $null 78 | $script:show_msgs = $false 79 | $script:write_to_serial = $false 80 | 81 | # Functions 82 | function _AvailabilityReplica { 83 | Write-Logger "Creating SqlAvailabilityReplica on $Global:hostname" 84 | $initialized_nodes = @() 85 | 86 | # Find the version of SQL Server running 87 | $Srv = Get-Item SQLSERVER:\SQL\$($Global:hostname)\DEFAULT 88 | $Version = ($Srv.Version) 89 | 90 | try { 91 | ForEach($node in $Script:all_nodes) { 92 | # Create an in-memory representation of the primary replica 93 | $initialized_nodes += New-SqlAvailabilityReplica ` 94 | -Name $node ` 95 | -EndpointURL "TCP://$($node).$($Script:domain_name):5022" ` 96 | -AvailabilityMode SynchronousCommit ` 97 | -FailoverMode Automatic ` 98 | -Version $Version ` 99 | -AsTemplate 100 | } 101 | return $initialized_nodes 102 | } 103 | catch{ 104 | Write-Logger $_.Exception.GetType().FullName 105 | Write-Logger "$_.Exception.Message" 106 | return $false 107 | } 108 | } 109 | 110 | function _BackUpDataBase { 111 | # Backup my database and its log on the primary 112 | Write-Logger "Creating backups of database $Script:db_name on $script:node2" 113 | try { 114 | # backup DB 115 | $backupDB = "\\$script:node2\$Script:db_folder_backup\$($Script:db_name)_db.bak" 116 | Backup-SqlDatabase ` 117 | -Database $Script:db_name ` 118 | -BackupFile $backupDB ` 119 | -ServerInstance $Global:hostname ` 120 | -Initialize 121 | # Backup log 122 | $backupLog = "\\$script:node2\$Script:db_folder_backup\$($db_name)_log.bak" 123 | Backup-SqlDatabase ` 124 | -Database $Script:db_name ` 125 | -BackupFile $backupLog ` 126 | -ServerInstance $Global:hostname ` 127 | -BackupAction Log -Initialize 128 | return $true 129 | } 130 | catch { 131 | Write-Logger $_.Exception.GetType().FullName 132 | Write-Logger "$_.Exception.Message" 133 | return $false 134 | } 135 | } 136 | 137 | function _CreateEndPoint { 138 | Write-Logger "Creating endpoint on node $Global:hostname" 139 | # Creating endpoint 140 | try { 141 | $endpoint = New-SqlHadrEndpoint "Hadr_endpoint" ` 142 | -Port 5022 ` 143 | -Path "SQLSERVER:\SQL\$Global:hostname\Default" 144 | Set-SqlHadrEndpoint -InputObject $endpoint -State "Started" 145 | return $true 146 | } 147 | catch [System.Data.SqlClient.SqlException] { 148 | Write-Logger "'Hadr_endpoint' already exists." 149 | return $true 150 | } 151 | catch { 152 | Write-Logger $_.Exception.GetType().FullName 153 | Write-Logger "$_.Exception.Message" 154 | return $false 155 | } 156 | } 157 | 158 | function _DBPermission { 159 | # Grant connect permissions to the endpoints 160 | $query = " ` 161 | IF SUSER_ID('$($script:remote_nodes)') IS NULL CREATE LOGIN [$($script:remote_nodes)] FROM WINDOWS ` 162 | GO 163 | GRANT CONNECT ON ENDPOINT::[Hadr_endpoint] TO [$($script:remote_nodes)] ` 164 | GO ` 165 | IF EXISTS(SELECT * FROM sys.server_event_sessions WHERE name='AlwaysOn_health') ` 166 | BEGIN ` 167 | ALTER EVENT SESSION [AlwaysOn_health] ON SERVER WITH (STARTUP_STATE=ON); ` 168 | END ` 169 | IF NOT EXISTS(SELECT * FROM sys.dm_xe_sessions WHERE name='AlwaysOn_health') ` 170 | BEGIN ` 171 | ALTER EVENT SESSION [AlwaysOn_health] ON SERVER STATE=START; ` 172 | END ` 173 | GO " 174 | 175 | try { 176 | Write-Logger "$Global:hostname - Granting permission to endpoint" 177 | Write-Logger $query 178 | Invoke-Sqlcmd -Query $query 179 | return $true 180 | } 181 | catch { 182 | Write-Logger $_.Exception.GetType().FullName 183 | Write-Logger "$_.Exception.Message" 184 | return $false 185 | } 186 | } 187 | 188 | function _EnableAlwaysOn { 189 | # Enable Always-On on all Server nodes 190 | ForEach($node in $Script:all_nodes){ 191 | try { 192 | Write-Logger "Sleeping for 10s ...." 193 | Start-Sleep -s 10 194 | Write-Logger "Trying to enable AlwaysOn feature for $node" 195 | Enable-SqlAlwaysOn -ServerInstance $node -Force 196 | Write-Logger "-- AlwaysOn feature turned on for $node .. --" 197 | } 198 | catch [Microsoft.SqlServer.Management.Smo.FailedOperationException] { 199 | Write-Logger "ChangeHADRService failed for Service 'MSSQLSERVER' on node: $node" 200 | Write-logger "$Script:cluster_name is not setup correctly." 201 | return $false 202 | } 203 | catch { 204 | Write-Logger $_.Exception.GetType().FullName 205 | Write-Logger "$_.Exception.Message" 206 | return $false 207 | } 208 | } 209 | return $true 210 | } 211 | 212 | function _NewCluster { 213 | 214 | Write-Logger "Setting up cluster $Script:cluster_name for nodes $Script:all_nodes_fqdn and ips $Script:static_ip" 215 | # Create the cluster 216 | try { 217 | $result = New-Cluster -Name $Script:cluster_name -Node $Script:all_nodes_fqdn ` 218 | -NoStorage -StaticAddress $Script:static_ip 219 | Write-Logger "Result for setup cluster: $result" 220 | return $true 221 | } 222 | catch { 223 | Write-Logger "** Failed to setup cluster: $Script:cluster_name ** " 224 | Write-Logger $_.Exception.GetType().FullName 225 | Write-Logger "$_.Exception.Message" 226 | return $false 227 | } 228 | } 229 | 230 | function _RestoreDataBase { 231 | Write-Logger "Restoring backups of database $Script:db_name on $Global:hostname" 232 | 233 | try { 234 | $backupDB = "\\$script:node2\$Script:db_folder_backup\$($Script:db_name)_db.bak" 235 | Write-Logger "Restoring DB from $backupDB" 236 | Restore-SqlDatabase ` 237 | -Database $Script:db_name ` 238 | -BackupFile $backupDB ` 239 | -ServerInstance $Global:hostname ` 240 | -NoRecovery -ReplaceDatabase 241 | # Restore Backup log 242 | $backupLog = "\\$script:node2\$Script:db_folder_backup\$($db_name)_log.bak" 243 | Write-Logger "Restoring Logs from $backupLog" 244 | Restore-SqlDatabase ` 245 | -Database $Script:db_name ` 246 | -BackupFile $backupLog ` 247 | -ServerInstance $node2 ` 248 | -RestoreAction Log ` 249 | -NoRecovery 250 | return $true 251 | } 252 | catch { 253 | Write-Logger $_.Exception.GetType().FullName 254 | Write-Logger "$_.Exception.Message" 255 | return $false 256 | } 257 | } 258 | 259 | function CheckIfNode1 { 260 | <# 261 | .SYNOPSIS 262 | Checks if the current host is Node1 263 | .DESCRIPTION 264 | If the current host is node1 we do treat it as primary 265 | .EXAMPLE 266 | CheckIfNode1 267 | #> 268 | 269 | if ($Global:hostname.EndsWith(1)){ 270 | return $true 271 | } 272 | } 273 | 274 | function ConfigureAvailabiltyGroup { 275 | <# 276 | .SYNOPSIS 277 | ConfigureAvailabiltyGroup 278 | .DESCRIPTION 279 | Configures the newly created availability group 280 | .EXAMPLE 281 | ConfigureAvailabiltyGroup 282 | #> 283 | $initialized_nodes = @() 284 | if ((_CreateEndPoint) -and (_DBPermission)) { 285 | Write-Logger "-- Availability endpoints are configured for all nodes. --" 286 | 287 | if (CheckIfNode1) { # Backup Primary database 288 | if (_BackUpDataBase) { 289 | UpdateSubWaiter -key "$Script:backup_key_path/success/done" 290 | } 291 | else { 292 | UpdateSubWaiter -key "$Script:backup_key_path/failure/failed" 293 | return $false 294 | } 295 | } 296 | else { 297 | # Restore primary db on other nodes 298 | if ((WaitForRuntime -path "$Script:backup_key_path/success/done" -timeout 12)) { 299 | if (_RestoreDataBase) { 300 | Write-Logger "$Script:db_name database restored successfully on $Global:hostname" 301 | UpdateSubWaiter -key "$Script:initdb_key_path/success/done" 302 | } 303 | else { 304 | Write-Logger "Failed to restore $Script:db_name on $Global:hostname" 305 | UpdateSubWaiter -key "$Script:initdb_key_path/failure/failed" 306 | return $false 307 | } 308 | } 309 | else { 310 | Write-Logger "TimeOut exceeded while waiting on node(s) to finish backup/restore operation." 311 | return $false 312 | } 313 | } 314 | 315 | # Create the New-SqlAvailabilityReplica 316 | if ((WaitForRuntime -path "$Script:initdb_key_path/success/done" -timeout 12)) { 317 | if (CheckIfNode1) { 318 | $initialized_nodes = _AvailabilityReplica 319 | if ($initialized_nodes) { 320 | Write-Logger ("Availability replica has been set.") 321 | UpdateSubWaiter -key "$Script:replica_key_path/success/done" 322 | 323 | # Create the availability group 324 | Write-Logger "-- Create Availability Group: $Script:name_ag --" 325 | try { 326 | New-SqlAvailabilityGroup ` 327 | -Name $Script:name_ag ` 328 | -Path "SQLSERVER:\SQL\$($Global:hostname)\DEFAULT" ` 329 | -AvailabilityReplica $initialized_nodes ` 330 | -Database $Script:db_name 331 | } 332 | catch{ 333 | Write-Logger "** Failed to create SqlAvailabilityGroup. **" 334 | Write-Logger $_.Exception.GetType().FullName 335 | Write-Logger "$_.Exception.Message" 336 | return $false 337 | } 338 | 339 | # Join other nodes to availability group. 340 | Write-Logger "-- Joining nodes to: $Script:name_ag --" 341 | ForEach($node in $Script:all_nodes) { 342 | Write-Logger " adding $node to the $Script:name_ag" 343 | if ($node.EndsWith(1)) { 344 | Write-Logger "Primary $node does not needed to be added to $Script:name_ag." 345 | } 346 | else { 347 | try { 348 | Join-SqlAvailabilityGroup ` 349 | -Path "SQLSERVER:\SQL\$($node)\DEFAULT" ` 350 | -Name $Script:name_ag 351 | } 352 | catch { 353 | Write-Logger "** Failed to join $node in AvailabilityGroup. **" 354 | Write-Logger $_.Exception.GetType().FullName 355 | Write-Logger "$_.Exception.Message" 356 | } 357 | 358 | # Join the secondary database to the availability group. 359 | Write-Logger "-- Join DB in $node to Availability Group. --" 360 | try { 361 | Add-SqlAvailabilityDatabase ` 362 | -Path "SQLSERVER:\SQL\$($node)\DEFAULT\AvailabilityGroups\$($Script:name_ag)" ` 363 | -Database $Script:db_name 364 | } 365 | catch { 366 | Write-Logger "** Failed to join $Script:db_name on $node to $Script:name_ag. **" 367 | Write-Logger $_.Exception.GetType().FullName 368 | Write-Logger "$_.Exception.Message" 369 | } 370 | } 371 | } 372 | 373 | # Create the listener 374 | Write-Logger "-- Create Listener with IPs: $Script:static_listner_ip. --" 375 | try { 376 | New-SqlAvailabilityGroupListener ` 377 | -Name $name_ag_listener ` 378 | -StaticIp $Script:static_listner_ip ` 379 | -Path SQLSERVER:\SQL\$($Global:hostname)\DEFAULT\AvailabilityGroups\$($Script:name_ag) 380 | } 381 | catch{ 382 | Write-Logger "** Failed to add listeners to $Script:name_ag. **" 383 | Write-Logger $_.Exception.GetType().FullName 384 | Write-Logger "$_.Exception.Message" 385 | return $false 386 | } 387 | } 388 | else { 389 | Write-Logger "** Failed to initialize all db in sqlcluster **" 390 | UpdateSubWaiter -key "$Script:replica_key_path/failure/failed" 391 | return $false 392 | } 393 | } 394 | } 395 | else { 396 | Write-Logger "TimeOut exceeded while waiting on nodes to initialize db." 397 | return $false 398 | } 399 | 400 | # Check availability group 401 | if ((WaitForRuntime -path "$Script:replica_key_path/success/done" -timeout 12)) { 402 | Write-Logger "Waiting.." 403 | } 404 | else { 405 | return $true 406 | } 407 | } 408 | else { 409 | Write-Logger "Failed to create endpoints" 410 | return $false 411 | } 412 | } 413 | 414 | function CreateShares { 415 | <# 416 | .SYNOPSIS 417 | Creates Folders and shares on local machine 418 | .DESCRIPTION 419 | Creates folder and shares for SQL server HA needs 420 | .EXAMPLE 421 | CreateShares 422 | #> 423 | 424 | if (Test-Path -path $shares_already_created_reg) { 425 | Write-Log "Shares are already created. Nothing to do here..." 426 | } 427 | else { 428 | # Configure SQL Folders 429 | Write-Log "Create SQL Share Folders $Script:db_folder_data & $Script:db_folder_log" 430 | New-Item -ItemType directory -Path "C:\$Script:db_folder_data" 431 | New-Item -ItemType directory -Path "C:\$Script:db_folder_log" 432 | 433 | if ($Global:hostname.EndsWith(2)){ # Create backup share on node2 only 434 | Write-Log "Creating backup share $Script:db_folder_backup as $global:hostname is not the primary node." 435 | New-Item -ItemType directory -Path "C:\$Script:db_folder_backup" 436 | try { 437 | New-SMBShare -Name "$Script:db_folder_backup" -Path "C:\$Script:db_folder_backup" ` 438 | -FullAccess 'Everyone' 439 | } 440 | catch { 441 | _PrintError 442 | } 443 | 444 | # Enable CredSSP on localhost and disable name checking 445 | if (!(CheckIfNode1)) { 446 | Write-Log "Enable CredSSP Client on $Global:hostname" 447 | try { 448 | Enable-WSManCredSSP Client -DelegateComputer * -Force 449 | # Wait 15 secs before enabling CredSSP in both servers 450 | # On occasions got errors when running the command that follows without waiting 451 | Start-Sleep -s 15 452 | } 453 | catch { 454 | _PrintError 455 | } 456 | } 457 | } 458 | # Enable CredSSP Server in remote nodes 459 | Write-Log "Enable CredSSP Server nodes on: $Global:Hostname" 460 | Enable-WSManCredSSP Server -Force 461 | # On all Nodes 462 | Write-ToReg $shares_already_created_reg 463 | } 464 | } 465 | 466 | function CreateTestDB { 467 | <# 468 | .SYNOPSIS 469 | Create a testDB 470 | .DESCRIPTION 471 | Creates a TestDB on the machines. Script based on: 472 | https://github.com/sqlthinker/dotnet-docs-samples/blob/master/compute/sqlserver/powershell/create-availability-group.ps1 473 | .EXAMPLE 474 | CreateTestDB 475 | #> 476 | 477 | $sql_data = "C:\$Script:db_folder_data" # Directory to store the database data files 478 | $sql_log = "C:\$Script:db_folder_log" # Directory to store the database transaction log files 479 | $data_size = 1024 # Initial size of the database in MB 480 | $data_growth = 256 # Auto growth size of the database in MB 481 | $log_size = 1024 # Initial size of the transaction log in MB 482 | $log_growth = 256 483 | 484 | try { 485 | Write-Log "Disable Name Checking on $Global:hostname" 486 | Import-Module SQLPS -DisableNameChecking 487 | $objServer = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server ` 488 | -ArgumentList 'localhost' 489 | } 490 | catch { 491 | _PrintError 492 | return $false 493 | } 494 | 495 | # Only continue if the database does not exist 496 | $objDB = $objServer.Databases[$Script:db_name] 497 | if (!($objDB)) { 498 | Write-Log "$Global:hostname - Creating the database $db_name" 499 | $objDB = New-Object ` 500 | -TypeName Microsoft.SqlServer.Management.Smo.Database($objServer, $db_name) 501 | 502 | # Create the primary file group and add it to the database 503 | $objPrimaryFG = New-Object ` 504 | -TypeName Microsoft.SqlServer.Management.Smo.Filegroup($objDB, 'PRIMARY') 505 | $objDB.Filegroups.Add($objPrimaryFG) 506 | 507 | # Create a single data file and add it to the Primary file group 508 | $dataFileName = $Script:db_name + '_Data' 509 | $objData = New-Object ` 510 | -TypeName Microsoft.SqlServer.Management.Smo.DataFile($objPrimaryFG, $dataFileName) 511 | $objData.FileName = $sql_data + '\' + $dataFileName + '.mdf' 512 | $objData.Size = ($data_size * 1024) 513 | $objData.GrowthType = 'KB' 514 | $objData.Growth = ($data_growth * 1024) 515 | $objData.IsPrimaryFile = 'true' 516 | $objPrimaryFG.Files.Add($objData) 517 | 518 | # Create the log file and add it to the database 519 | $logName = $Script:db_name + '_Log' 520 | $objLog = New-Object Microsoft.SqlServer.Management.Smo.LogFile($objDB, $logName) 521 | $objLog.FileName = $sql_log + '\' + $logName + '.ldf' 522 | $objLog.Size = ($log_size * 1024) 523 | $objLog.GrowthType = 'KB' 524 | $objLog.Growth = ($log_growth * 1024) 525 | $objDB.LogFiles.Add($objLog) 526 | 527 | # Create the database 528 | $objDB.Script() # Show a script with the command we are about to run 529 | $objDB.Create() # Create the database 530 | $objDB.SetOwner('sa') # Change the owner to sa 531 | } 532 | else { 533 | Write-Log "$Script:db_name DB already exists on $Global:hostname. Skipping ..." 534 | } 535 | } 536 | 537 | function InstallServerComponents { 538 | <# 539 | .SYNOPSIS 540 | Install all components needed for SQL Server Setup. 541 | .DESCRIPTION 542 | All install-windows feature and modules required on all nodes. 543 | .EXAMPLE 544 | InstallServerComponents 545 | #> 546 | 547 | Write-Log "Installing Server Components ..." 548 | try { 549 | # We may need to remove AD objects, so we will need the RSAT-AD-PowerShell 550 | Install-WindowsFeature RSAT-AD-PowerShell 551 | Install-WindowsFeature Failover-Clustering -IncludeManagementTools 552 | return $true 553 | } 554 | catch { 555 | Write-Log $_.Exception.GetType().FullName -error 556 | Write-Log "$_.Exception.Message" 557 | return $false 558 | } 559 | } 560 | 561 | function JoinDomain { 562 | <# 563 | .SYNOPSIS 564 | Join current machine to domain. 565 | .DESCRIPTION 566 | Attempts to join the current machine domain 567 | .EXAMPLE 568 | JoinDomain 569 | #> 570 | 571 | Write-Log "Fetching Domain join parameters." 572 | $SA_PASSWORD = (ConvertTo-SecureString (_FetchFromMetaData ` 573 | -property 'attributes/c2d-property-sa-password') -AsPlainText -Force) 574 | 575 | $credential = New-Object System.Management.Automation.PSCredential($script:domain_service_account, $SA_PASSWORD) 576 | Write-Log "Attempting to join $global:hostname to $Script:domain_name." 577 | try { 578 | Add-Computer -DomainName $Script:domain_name -Credential $credential 579 | return $true 580 | } 581 | catch { 582 | _PrintError 583 | return $false 584 | } 585 | } 586 | 587 | function SetIP{ 588 | <# 589 | .SYNOPSIS 590 | Set local machine IP, Gateway and Firewall 591 | .DESCRIPTION 592 | Set IP address, Gateway, and Firewall 593 | .EXAMPLE 594 | SetIP 595 | #> 596 | 597 | Write-Log "Getting Current IP settings on $global:hostname." 598 | try { 599 | $current_ip = (Get-NetIPConfiguration | ` 600 | Where InterfaceAlias -eq 'Ethernet').IPv4Address.IPAddress 601 | Write-Log "Current IP Address: $current_ip" 602 | 603 | $current_gateway = (Get-NetIPConfiguration | ` 604 | Where InterfaceAlias -eq 'Ethernet').Ipv4DefaultGateway.NextHop 605 | Write-Log "Current GateWay Address: $current_gateway" 606 | } 607 | catch { 608 | _PrintError 609 | return $false 610 | } 611 | 612 | try { 613 | Write-Log "Setting Static IP on $global:hostname." 614 | _RunExternalCMD netsh interface ip set address name=Ethernet static $current_ip 255.255.0.0 $current_gateway 1 615 | 616 | Start-Sleep -Seconds 10 617 | 618 | Write-Log "Setting DNS $global:hostname." 619 | _RunExternalCMD netsh interface ip set dns Ethernet static 10.0.0.100 620 | 621 | Write-Log "Opening up SQL-Server specific Firewall ports $global:hostname." 622 | _RunExternalCMD netsh advfirewall firewall add rule ` 623 | name="Open Port 5022 for Availability Groups" dir=in action=allow protocol=TCP localport=5022 624 | _RunExternalCMD netsh advfirewall firewall add rule ` 625 | name="Open Port 1433 for SQL Server" dir=in action=allow protocol=TCP localport=1433 626 | } 627 | catch { 628 | _PrintError 629 | return $false 630 | } 631 | return $true 632 | } 633 | 634 | function SetScriptVar { 635 | <# 636 | .SYNOPSIS 637 | Initialize all necessary script variables. 638 | .DESCRIPTION 639 | Called once at the beginning of the script to initialize $Script:x 640 | .EXAMPLE 641 | SetScriptVar 642 | #> 643 | 644 | # Set Service Account 645 | $Script:service_account = _FetchFromMetaData -property 'attributes/c2d-property-sa-account' 646 | 647 | # Set DomainName Properties 648 | $Script:domain_name = _FetchFromMetaData -property 'attributes/c2d-property-domain-dns-name' 649 | $Script:domain_bios_name = $Script:domain_name.split(".")[0] 650 | $script:domain_service_account = "$Script:domain_bios_name\$Script:service_account" 651 | $script:sa_password = _FetchFromMetaData -property 'attributes/c2d-property-sa-password' 652 | 653 | # Get all nodes 654 | $Script:all_nodes = ((_FetchFromMetaData -property 'attributes/sql-nodes').split("|")).Where({ $_ -ne "" }) 655 | 656 | # Add FQDN and get static ip address 657 | $ip_count = 1 658 | ForEach ($host_node in $all_nodes) { 659 | $Script:all_nodes_fqdn += "$host_node.$Script:domain_name" 660 | $Script:static_ip += "10.$ip_count.1.4" 661 | $Script:static_listner_ip += "10.$ip_count.1.5/255.255.0.0" 662 | $ip_count++ 663 | if (!($host_node -eq $Global:hostname)) { 664 | $script:remote_nodes += "$($Script:domain_bios_name)\$($host_node)`$" 665 | } 666 | } 667 | 668 | $script:node2 = $all_nodes[1] 669 | 670 | # Create PS CRED object 671 | $Pwd = ConvertTo-SecureString $script:sa_password -AsPlainText -Force 672 | $Script:cred_obj = New-Object System.Management.Automation.PSCredential $script:domain_service_account, $Pwd 673 | } 674 | 675 | function SetupCluster { 676 | <# 677 | .SYNOPSIS 678 | Setup New Cluster 679 | .DESCRIPTION 680 | Setups a new cluster and enables availability group 681 | .EXAMPLE 682 | SetupCluster 683 | #> 684 | $if_exists = $null 685 | $retry_attempt = $null 686 | $no_of_try = 687 | 688 | # This loop is to catch an edge case where _NewCluster setup exists 689 | # without any error message and does not setup cluster. 690 | for ($retry_attempt=0; $retry_attempt -le 1; $retry_attempt++) { 691 | if(_NewCluster) { # Run the setup command. 692 | try { 693 | $if_exists = (Get-Cluster -ErrorAction SilentlyContinue).Name 694 | if ($if_exists) { 695 | Write-Logger "-- $if_exists new cluster setup complete. --" 696 | break 697 | } 698 | } 699 | catch [System.Management.Automation.PropertyNotFoundException] { 700 | # This block will run if NewCluster ran without any errors. 701 | Write-Logger "## Cluster $Script:cluster_name is not configured. Will retry again, attempt: $retry_attempt .. ##" 702 | continue 703 | } 704 | catch { 705 | Write-Logger $_.Exception.GetType().FullName 706 | Write-Logger "$_.Exception.Message" 707 | return $false 708 | } 709 | } 710 | else { # if the NewCluster command fails exit 711 | Write-Logger "** NewCluster setup failed. **" 712 | return $false 713 | } 714 | } 715 | 716 | if($if_exists) { 717 | if(_EnableAlwaysOn){ 718 | Write-Logger "-- Always on enabled for all nodes in cluster. --" 719 | return $true 720 | } 721 | else{ 722 | Write-Logger "** Turning on AlwaysOn feature for nodes:$Script:all_nodes. **" 723 | return $false 724 | } 725 | } 726 | else{ 727 | Write-Logger "** Cluster setup failed for unknown reason. **" 728 | return $false 729 | } 730 | } 731 | 732 | function UpdateSubWaiter { 733 | <# 734 | .SYNOPSIS 735 | UpdateSubWaiter 736 | .DESCRIPTION 737 | Updates sub runtime waiters. 738 | .EXAMPLE 739 | UpdateSubWaiter -key 740 | #> 741 | param ( 742 | [Alias('key')] 743 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 744 | $key_path 745 | ) 746 | 747 | CreateRunTimeVariable -config_path (_GetRuntimeConfig) -var_name "$key_path" /failure/failed 748 | #TODO remove the following 2 lines 749 | $path=_GetRuntimeConfig 750 | Write-Logger "RuntimeConfigPath is $path" 751 | } 752 | 753 | 754 | ## Mainregi 755 | 756 | # Initialize Script variables 757 | SetScriptVar 758 | 759 | # Set registry paths 760 | $sql_on_domain_reg = "HKLM:\SOFTWARE\Google\SQLOnDomain" 761 | $sql_configured_reg = "HKLM:\SOFTWARE\Google\SQLServerConfigured" 762 | $sql_server_task = "HKLM:\SOFTWARE\Google\SQLServerTask" 763 | $shares_already_created_reg = "HKLM:\SOFTWARE\Google\SharesCreated" 764 | 765 | if($as_job){ # Run as Scheduled Task. 766 | if (Test-Path -path $sql_configured_reg) { 767 | Write-Logger "$global:hostname sql node is already configured. Nothing to do here." 768 | exit 0 769 | } 770 | 771 | # Lets create the cluster as a Scheduled task in the service account context 772 | Write-Logger "Attempting to install cluster on $Global:hostname" 773 | Write-Logger "-- Running as $env:UserName --" 774 | 775 | if (CheckIfNode1){ # This command runs on node1 776 | if (SetupCluster) { 777 | Write-Logger "Cluster setup was successful on $Global:hostname" 778 | Write-Logger "----------------------------" 779 | Write-Logger "SQL Cluster install finished on $Global:hostname." 780 | Write-Logger "----------------------------" 781 | UpdateSubWaiter -key "$Script:cluster_key_path/success/done" 782 | } 783 | else { 784 | Write-Logger "** SetupCluster step failed ***" 785 | UpdateSubWaiter -key "$Script:cluster_key_path/failure/failed" 786 | UpdateRunTimeWaiter -path status -failure 787 | exit 788 | } 789 | } 790 | else { # All Secondary nodes 791 | Write-Logger "Waiting for cluster setup." 792 | if ((WaitForRuntime -path "$Script:cluster_key_path/success/done" -timeout 10)){ 793 | Write-Logger "Cluster Setup finished on primary node" 794 | } 795 | else { 796 | Write-Logger "** Something went wrong during primary cluster setup. **" 797 | UpdateRunTimeWaiter -path status -failure 798 | exit 799 | } 800 | } 801 | 802 | # Run this for all nodes 803 | if (ConfigureAvailabiltyGroup){ 804 | # All Done 805 | Write-Logger "----------------------------" 806 | Write-Logger " AG install finished on $Global:hostname." 807 | Write-Logger "----------------------------" 808 | Write-ToReg $sql_configured_reg 809 | UpdateRunTimeWaiter -path status 810 | exit 811 | } 812 | else { 813 | Write-Logger "*** Failed to create Availability Group. ***" 814 | UpdateRunTimeWaiter -failure 815 | UpdateRunTimeWaiter -path status -failure 816 | } 817 | } 818 | 819 | 820 | # Do SQL Server Installs 821 | if (Test-Path -path $sql_on_domain_reg) { 822 | # Check if first bootup. If yes, do following steps. 823 | Write-Log "$global:hostname sql node is joined to the $Script:domain_name domain." -important 824 | } 825 | else { # Join the machine to the domain 826 | Write-Log "We are live from SQL Server nodes." 827 | # Set Static IP Address 828 | if (SetIP) { 829 | UpdateRunTimeWaiter 830 | } 831 | else { 832 | UpdateRunTimeWaiter -failure 833 | } 834 | if (JoinDomain) { 835 | UpdateRunTimeWaiter 836 | # Create registry key so this block is not run again. 837 | Write-ToReg $sql_on_domain_reg 838 | 839 | Write-Log "Rebooting $global:hostname" 840 | Restart-Computer 841 | exit 842 | } 843 | else { 844 | UpdateRunTimeWaiter -failure 845 | } 846 | } 847 | 848 | # Configure SQL Server after domain join 849 | if (Test-Path -path $sql_configured_reg) { 850 | Write-Log "$global:hostname sql node is already configured. Nothing to do here." -important 851 | exit 0 852 | } 853 | elseif ($task_name -and (!(Test-Path $sql_server_task ))) { 854 | Write-Log "Need to configure node for fail-over clustering." 855 | CreateShares 856 | InstallServerComponents 857 | Write-Log "Installed all necessary components" 858 | if (CheckIfNode1){ # Create TestDB on Node1 859 | Write-Log "Creating Local Database" -important 860 | CreateTestDB 861 | } 862 | 863 | # First Check if the scheduled task already exists? 864 | $sc_task = Get-ScheduledTask -TaskName $task_name -ErrorAction SilentlyContinue 865 | if ($sc_task) { 866 | Write-Log "-- $task_name scheduled task already exists. --" 867 | } 868 | else { # Create the scheduled task 869 | Write-Log "Create schtask: $task_name with file $PSCommandPath" 870 | CreateSCTask -name $task_name -user $script:domain_service_account -password $script:sa_password -file $PSCommandPath 871 | Start-Sleep -Seconds 5 872 | 873 | # Create registry key so this block is not run again 874 | Write-ToReg $sql_server_task 875 | 876 | Start-ScheduledTask -TaskName $task_name 877 | Write-Log "Scheduled task $task_name finished running." 878 | } 879 | } 880 | else { 881 | Write-Log "All SQL steps are done. For cluster setup run $PSCommandPath -AsJob" 882 | } 883 | -------------------------------------------------------------------------------- /powershell/comprehensive-runtime-config.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Function Set-RuntimeConfigVariable { 18 | Param( 19 | [Parameter(Mandatory=$True)][String] $ConfigPath, 20 | [Parameter(Mandatory=$True)][String] $Variable, 21 | [Parameter(Mandatory=$True)][String] $Text 22 | ) 23 | 24 | $Auth = $(gcloud auth print-access-token) 25 | 26 | $Path = "$ConfigPath/variables" 27 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$Path" 28 | 29 | $Json = (@{ 30 | name = "$Path/$Variable" 31 | text = $Text 32 | } | ConvertTo-Json) 33 | 34 | $Headers = @{ 35 | Authorization = "Bearer " + $Auth 36 | } 37 | 38 | $Params = @{ 39 | Method = "POST" 40 | Headers = $Headers 41 | ContentType = "application/json" 42 | Uri = $Url 43 | Body = $Json 44 | } 45 | 46 | Try { 47 | Return Invoke-RestMethod @Params 48 | } 49 | Catch { 50 | $Reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) 51 | $ErrResp = $Reader.ReadToEnd() | ConvertFrom-Json 52 | $Reader.Close() 53 | Return $ErrResp 54 | } 55 | 56 | } 57 | 58 | Function Get-RuntimeConfigWaiter { 59 | Param( 60 | [Parameter(Mandatory=$True)][String] $ConfigPath, 61 | [Parameter(Mandatory=$True)][String] $Waiter 62 | ) 63 | 64 | $Auth = $(gcloud auth print-access-token) 65 | 66 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 67 | $Headers = @{ 68 | Authorization = "Bearer " + $Auth 69 | } 70 | $Params = @{ 71 | Method = "GET" 72 | Headers = $Headers 73 | Uri = $Url 74 | } 75 | Write-Host "$Url" 76 | 77 | Return Invoke-RestMethod @Params 78 | } 79 | 80 | Function Wait-RuntimeConfigWaiter { 81 | Param( 82 | [Parameter(Mandatory=$True)][String] $ConfigPath, 83 | [Parameter(Mandatory=$True)][String] $Waiter, 84 | [int] $Sleep = 60 85 | ) 86 | $RuntimeWaiter = $Null 87 | 88 | Write-Host $ConfigPath/waiters/$Waiter 89 | 90 | While (($RuntimeWaiter -eq $Null) -Or (-Not $RuntimeWaiter.done)) { 91 | $RuntimeWaiter = Get-RuntimeConfigWaiter -ConfigPath $ConfigPath -Waiter $Waiter 92 | If (-Not $RuntimeWaiter.done) { 93 | Write-Host "Waiting for [$ConfigPath/waiters/$Waiter]..." 94 | Sleep $Sleep 95 | } 96 | } 97 | Return $RuntimeWaiter 98 | } 99 | 100 | Function Create-RuntimeConfigWaiter { 101 | Param( 102 | [Parameter(Mandatory=$True)][String] $ConfigPath, 103 | [Parameter(Mandatory=$True)][String] $Waiter, 104 | [Parameter(Mandatory=$True)][String] $Timeout, 105 | [Parameter(Mandatory=$True)][String] $SuccessPath, 106 | [Parameter(Mandatory=$True)][Int] $SuccessCardinality, 107 | [Parameter(Mandatory=$False)][String] $FailurePath = "", 108 | [Parameter(Mandatory=$False)][Int] $FailureCardinality=0 109 | ) 110 | 111 | $RuntimeWaiter = $Null 112 | 113 | Write-Host $ConfigPath/waiters/$Waiter 114 | 115 | $Auth = $(gcloud auth print-access-token) 116 | 117 | 118 | if($FailurePath.Length -eq 0){ 119 | $Body = "{timeout: '" + $Timeout + "s', name: '$ConfigPath/waiters/$Waiter', success: { cardinality: { number: $SuccessCardinality, path: '$SuccessPath' }}}" 120 | }else{ 121 | $Body = "{timeout: '" + $Timeout + "s', name: '$ConfigPath/waiters/$Waiter', ` 122 | success: { cardinality: { number: $SuccessCardinality, path: '$SuccessPath' }}, ` 123 | failure: { cardinality: { number: $FailureCardinality, path: '$FailurePath' }}}" 124 | } 125 | 126 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters" 127 | 128 | $Headers = @{ 129 | Authorization="Bearer " + $Auth 130 | } 131 | $Params = @{ 132 | Method = "POST" 133 | Headers = $Headers 134 | Uri = $Url 135 | Body=$Body 136 | } 137 | Write-Host "$Url" 138 | # Write-Host "$Params" 139 | 140 | #Return Invoke-RestMethod $Params 141 | Return Invoke-RestMethod -Uri $Url -Headers $Headers -Method 'Post' -Body $Body -ContentType "application/json" 142 | #Return $RuntimeWaiter 143 | } 144 | 145 | Function Delete-RuntimeConfigWaiter { 146 | Param( 147 | [Parameter(Mandatory=$True)][String] $ConfigPath, 148 | [Parameter(Mandatory=$True)][String] $Waiter 149 | ) 150 | 151 | $RuntimeWaiter = $Null 152 | 153 | Write-Host $ConfigPath/waiters/$Waiter 154 | 155 | $Auth = $(gcloud auth print-access-token) 156 | 157 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters/$Waiter" 158 | 159 | $Headers = @{ 160 | Authorization="Bearer " + $Auth 161 | } 162 | 163 | Write-Host "$Url" 164 | 165 | Return Invoke-RestMethod -Uri $Url -Headers $Headers -Method 'Delete' 166 | } 167 | 168 | 169 | Function List-RuntimeConfigWaiter { 170 | Param( 171 | [Parameter(Mandatory=$True)][String] $ConfigPath 172 | ) 173 | 174 | $RuntimeWaiter = $Null 175 | 176 | Write-Host $ConfigPath/waiters 177 | 178 | $Auth = $(gcloud auth print-access-token) 179 | 180 | $Url = "https://runtimeconfig.googleapis.com/v1beta1/$ConfigPath/waiters" 181 | 182 | $Headers = @{ 183 | Authorization="Bearer " + $Auth 184 | } 185 | 186 | Write-Host "$Url" 187 | 188 | Return Invoke-RestMethod -Uri $Url -Headers $Headers -Method 'Get' 189 | } 190 | 191 | Function DeleteAllWaiters{ 192 | Param( 193 | [Parameter(Mandatory=$True)][String] $ConfigPath 194 | ) 195 | 196 | $List = List-RuntimeConfigWaiter -ConfigPath $RuntimeConfig 197 | 198 | foreach($waiter in $List.waiters){ 199 | $waiterName=$waiter.name.Substring($waiter.name.LastIndexOf("/")+1, $waiter.name.Length - $waiter.name.LastIndexOf("/")-1) 200 | Write-Host $waiterName 201 | Delete-RuntimeConfigWaiter -ConfigPath $ConfigPath -Waiter $waiterName 202 | } 203 | } 204 | 205 | $RuntimeConfig = "projects/{project-name}/configs/acme-config" 206 | $Waiter = "waiter41" 207 | 208 | $Result = List-RuntimeConfigWaiter -ConfigPath $RuntimeConfig 209 | 210 | foreach($waiter in $Result.waiters){ 211 | Write-Host $waiter 212 | } 213 | 214 | $DeleteResult=DeleteAllWaiters -ConfigPath $RuntimeConfig 215 | 216 | try{ 217 | Delete-RuntimeConfigWaiter -ConfigPath $RuntimeConfig ` 218 | -Waiter $Waiter 219 | }Catch{ 220 | Write-Host "Error" 221 | Write-Host $_.Exception.Message 222 | } 223 | 224 | 225 | try{ 226 | Create-RuntimeConfigWaiter -ConfigPath $RuntimeConfig ` 227 | -Waiter $Waiter ` 228 | -Timeout 100 ` 229 | -SuccessPath 'bootstrap/acme-sandbox-win-p-01/success' ` 230 | -SuccessCardinality 1 231 | #-FailurePath 'bootstrap/acme-sandbox-win-p-01/failure' ` 232 | #-FailureCardinality 1 233 | }Catch{ 234 | Write-Host "Error" 235 | Write-Host $_.Exception.Message 236 | } 237 | 238 | 239 | try{ 240 | Wait-RuntimeConfigWaiter -ConfigPath $RuntimeConfig -Waiter $Waiter 241 | #$thewaiter = Get-RuntimeConfigWaiter -ConfigPath $RuntimeConfig -Waiter $Waiter 242 | Write-Host $thewaiter 243 | } Catch{ 244 | Write-Host "Error" 245 | Write-Host $_.Exception.Message 246 | } 247 | 248 | Write-Host "Bootstrap script ended..." 249 | -------------------------------------------------------------------------------- /powershell/templates/windows-stackdriver-setup.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | 19 | $tempdir = "c:\temp\" 20 | $tempdir = $tempdir.tostring() 21 | $appToMatch = 'Stackdriver*' 22 | $msiFile = "C:\Windows\system32\msiexec.exe" 23 | 24 | $LOG='c:\temp\install.log' 25 | 26 | #function to write debugging info to the console 27 | Function Write-SerialPort ([string] $message) { 28 | $port = new-Object System.IO.Ports.SerialPort COM1,9600,None,8,one 29 | $port.open() 30 | $port.WriteLine($message) 31 | $port.Close() 32 | } 33 | 34 | function Get-InstalledApps 35 | { 36 | if ([IntPtr]::Size -eq 4) { 37 | $regpath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' 38 | } 39 | else { 40 | $regpath = @( 41 | 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' 42 | 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' 43 | ) 44 | } 45 | Get-ItemProperty $regpath | .{process{if($_.DisplayName -and $_.UninstallString) { $_ } }} | Select DisplayName, Publisher, InstallDate, DisplayVersion, UninstallString |Sort DisplayName 46 | } 47 | 48 | Write-SerialPort "Environment passed in was: ${environment}" 49 | 50 | #is stackdriver installed 51 | $result = Get-InstalledApps | where {$_.DisplayName -like $appToMatch} 52 | 53 | #if we are not admin 54 | If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) 55 | { 56 | # Relaunch as an elevated process: 57 | Write-SerialPort "Elevating" 58 | Start-Process powershell.exe "-File",('"{0}"' -f $MyInvocation.MyCommand.Path) -Verb RunAs 59 | exit 60 | } 61 | 62 | Write-SerialPort "Prefix: ${environment} Name: ${projectname} comes from the project itself" 63 | Write-SerialPort "Elevated" 64 | 65 | Write-Host "Bootstrap script started..." 66 | 67 | 68 | Write-Host "Getting network config..." 69 | # reconfigure dhcp address as static to avoid warnings during dcpromo 70 | $IpAddr = Get-NetIPAddress -InterfaceAlias Ethernet 71 | $IpConf = Get-NetIPConfiguration -InterfaceAlias Ethernet 72 | 73 | Write-SerialPort "Fetching metadata parameters..." 74 | $Domain = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/domain-name 75 | #$NetBiosName = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/netbios-name 76 | $KmsKey = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/kms-key 77 | $KmsRegion = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/keyring-region 78 | $GcsPrefix = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix 79 | 80 | Write-SerialPort $Domain, $IpAddr, $GcsPrefix 81 | 82 | $tempPath = "c:\temp\" 83 | 84 | Write-Host "temp dir: $tempPath" 85 | 86 | if((Test-Path $tempPath) -eq 0){ 87 | New-Item -ItemType directory -Path c:\temp\ 88 | Write-Host "created c:\temp" 89 | } 90 | 91 | cd C:\temp\ 92 | 93 | $tempSDPath = "c:\temp\StackdriverMonitoring-GCM-46.exe" 94 | 95 | #get stackdriver 96 | if((Test-Path $tempSDPath) -eq 0){ 97 | Write-Host("Downloading stackdriver agent") 98 | invoke-webrequest https://repo.stackdriver.com/windows/StackdriverMonitoring-GCM-46.exe -OutFile $tempSDPath; 99 | }else{ 100 | Write-Host("Stackdriver Agent already downloaded") 101 | } 102 | 103 | Write-SerialPort("Get installed apps") 104 | 105 | $appToMatch = "StackdriverAgent" 106 | 107 | $result = Get-Process | where {$_.ProcessName -like $appToMatch} 108 | 109 | # Now running elevated so launch the script: 110 | If ($result -eq $null) { 111 | Write-Host "Running the Stackdriver install" 112 | .\StackdriverMonitoring-GCM-46.exe /S 113 | #msiexec.exe /qn /norestart /i $tempdir\$puppetInstall PUPPET_MASTER_SERVER=$PROJECT_PREFIX-puppet-p.c.$PROJECT_NAME.internal PUPPET_AGENT_ENVIRONMENT=$PUPPET_AGENT_ENVIRONMENT /l* $LOG 114 | }else{ 115 | Write-Host "Stackdriver is already installed" 116 | } 117 | 118 | Write-Host "Configuring windows-startup-script-url" 119 | $name = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/name 120 | $zone = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/zone 121 | $function = Invoke-RestMethod -Headers @{"Metadata-Flavor" = "Google"} -Uri http://169.254.169.254/computeMetadata/v1/instance/attributes/function 122 | 123 | if ($function -eq "pdc"){ 124 | Write-Host "Setting windows-startup-script-url in metadata to $GcsPrefix/powershell/bootstrap/primary-domain-controller-step-1.ps1" 125 | gcloud compute instances add-metadata "$name" --zone $zone --metadata windows-startup-script-url="$GcsPrefix/powershell/bootstrap/primary-domain-controller-step-1.ps1" 126 | }elseif ($function -eq "sql"){ 127 | Write-Host "Setting windows-startup-script-url in metadata to $GcsPrefix/powershell/bootstrap/install-sql-server-principal-step-1.ps1" 128 | gcloud compute instances add-metadata "$name" --zone $zone --metadata windows-startup-script-url="$GcsPrefix/powershell/bootstrap/domain-member.ps1" 129 | } 130 | 131 | Write-Host "Removing windows-startup-script-ps1 from metadata ..." 132 | gcloud compute instances remove-metadata "$name" --zone $zone --keys="windows-startup-script-ps1" 133 | 134 | Write-Host "Restarting computer after winstartup ..." 135 | Restart-Computer 136 | --------------------------------------------------------------------------------