cloud-mu 2.1.0beta → 3.0.0beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -28,6 +28,15 @@ module MU
28
28
 
29
29
  @@cloudformation_mode = false
30
30
 
31
+ # Return what we think of as a cloud object's habitat. In AWS, this means
32
+ # the +account_number+ in which it's resident. If this is not applicable,
33
+ # such as for a {Habitat} or {Folder}, returns nil.
34
+ # @param cloudobj [MU::Cloud::AWS]: The resource from which to extract the habitat id
35
+ # @return [String,nil]
36
+ def self.habitat(cloudobj)
37
+ cloudobj.respond_to?(:account_number) ? cloudobj.account_number : nil
38
+ end
39
+
31
40
  # Toggle ourselves into a mode that will emit a CloudFormation template
32
41
  # instead of actual infrastructure.
33
42
  # @param set [Boolean]: Set the mode
@@ -78,8 +87,8 @@ module MU
78
87
  # Stub method- there's no such thing as being "hosted" in a CloudFormation
79
88
  # environment. Calls {MU::Cloud::AWS.myRegion} to return sensible
80
89
  # values, if we happen to have AWS credentials configured.
81
- def self.myRegion
82
- MU::Cloud::AWS.myRegion
90
+ def self.myRegion(credentials = nil)
91
+ MU::Cloud::AWS.myRegion(credentials)
83
92
  end
84
93
 
85
94
  # Stub method- there's no such thing as being "hosted" in a CloudFormation
@@ -259,12 +268,6 @@ module MU
259
268
  "Properties" => {
260
269
  }
261
270
  }
262
- when "loggroup"
263
- desc = {
264
- "Type" => "AWS::EC2::LogGroup",
265
- "Properties" => {
266
- }
267
- }
268
271
  when "cache_subnets"
269
272
  desc = {
270
273
  "Type" => "AWS::ElastiCache::SubnetGroup",
@@ -649,7 +652,7 @@ module MU
649
652
  child_name = resource['#MUOBJECT'].cloudobj.cfm_name
650
653
  child_params = child_template[child_name]["Properties"]["Parameters"]
651
654
  child_params = Hash.new if child_params.nil?
652
- cfm_template["Parameters"].each { |key, data|
655
+ cfm_template["Parameters"].keys.each { |key|
653
656
  child_params[key] = { "Ref" => key }
654
657
  }
655
658
  MU::Cloud::CloudFormation.setCloudFormationProp(child_template[child_name], "Parameters", child_params)
@@ -94,10 +94,10 @@ module MU
94
94
  # @param port_range [String]: A port range descriptor (e.g. 0-65535). Only valid with udp or tcp.
95
95
  # @return [void]
96
96
  def addRule(hosts,
97
- proto: proto = "tcp",
98
- port: port = nil,
99
- egress: egress = false,
100
- port_range: port_range = "0-65535"
97
+ proto: "tcp",
98
+ port: nil,
99
+ egress: false,
100
+ port_range: "0-65535"
101
101
  )
102
102
  rule = Hash.new
103
103
  rule["proto"] = proto
@@ -146,7 +146,7 @@ module MU
146
146
  # Manufacture an EC2 security group. The second parameter, rules, is an
147
147
  # "ingress_rules" structure parsed and validated by MU::Config.
148
148
  #########################################################################
149
- def setRules(rules, add_to_self: add_to_self = false, ingress: ingress = true, egress: egress = false)
149
+ def setRules(rules, add_to_self: false, ingress: true, egress: false)
150
150
  return if rules.nil? or rules.size == 0
151
151
 
152
152
  if add_to_self
@@ -294,14 +294,6 @@ module MU
294
294
  MU::Cloud::AWS::FirewallRule.schema(config)
295
295
  end
296
296
 
297
- # Cloud-specific pre-processing of {MU::Config::BasketofKittens::servers}, bare and unvalidated.
298
- # @param server [Hash]: The resource to process and validate
299
- # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
300
- # @return [Boolean]: True if validation succeeded, False otherwise
301
- def self.validateConfig(server, configurator)
302
- MU::Cloud::AWS::FirewallRule.validateConfig(server, configurator)
303
- end
304
-
305
297
  # Does this resource type exist as a global (cloud-wide) artifact, or
306
298
  # is it localized to a region/zone?
307
299
  # @return [Boolean]
@@ -45,6 +45,7 @@ module MU
45
45
  "muID" => MU.deploy_id,
46
46
  "muUser" => MU.chef_user,
47
47
  "publicIP" => MU.mu_public_ip,
48
+ "mommaCatPort" => MU.mommaCatPort,
48
49
  "skipApplyUpdates" => @config['skipinitialupdates'],
49
50
  "windowsAdminName" => @config['windows_admin_username'],
50
51
  "resourceName" => @config["name"],
@@ -303,7 +304,7 @@ module MU
303
304
  role_name: baserole.role_name,
304
305
  policy_name: name
305
306
  )
306
- policies[name] = URI.unescape(resp.policy_document)
307
+ policies[name] = URI.decode_www_form(resp.policy_document)
307
308
  }
308
309
  }
309
310
  end
@@ -340,6 +341,14 @@ module MU
340
341
  nil
341
342
  end
342
343
 
344
+ # Return the date/time a machine image was created.
345
+ # @param ami_id [String]: AMI identifier of an Amazon Machine Image
346
+ # @param credentials [String]
347
+ # @return [DateTime]
348
+ def self.imageTimeStamp(ami_id, credentials: nil, region: nil)
349
+ MU::Cloud::AWS.imageTimeStamp(ami_id, credentials: credentials, region: region)
350
+ end
351
+
343
352
  # Cloud-specific configuration properties.
344
353
  # @param config [MU::Config]: The calling MU::Config object
345
354
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -166,6 +166,7 @@ module MU
166
166
  "deploySSHKey" => @deploy.ssh_public_key,
167
167
  "muID" => MU.deploy_id,
168
168
  "muUser" => MU.chef_user,
169
+ "mommaCatPort" => MU.mommaCatPort,
169
170
  "publicIP" => MU.mu_public_ip,
170
171
  "skipApplyUpdates" => @config['skipinitialupdates'],
171
172
  "windowsAdminName" => @config['windows_admin_username'],
@@ -253,8 +253,6 @@ module MU
253
253
  attr_reader :name
254
254
  attr_reader :cfm_template
255
255
  attr_reader :cfm_name
256
- attr_reader :name
257
-
258
256
 
259
257
  # @param parent [MU::Cloud::CloudFormation::VPC]: The parent VPC of this subnet.
260
258
  # @param config [Hash<String>]:
@@ -29,13 +29,61 @@ module MU
29
29
  @@authorizers = {}
30
30
  @@acct_to_profile_map = {}
31
31
  @@enable_semaphores = {}
32
+ @@readonly_semaphore = Mutex.new
33
+ @@readonly = {}
34
+
35
+ # Module used by {MU::Cloud} to insert additional instance methods into
36
+ # instantiated resources in this cloud layer.
37
+ module AdditionalResourceMethods
38
+ # Google Cloud url attribute, found in some form on most GCP cloud
39
+ # resources.
40
+ # @return [String]
41
+ def url
42
+ desc = cloud_desc
43
+ (desc and desc.self_link) ? desc.self_link : nil
44
+ end
45
+ end
32
46
 
33
47
  # Any cloud-specific instance methods we require our resource
34
48
  # implementations to have, above and beyond the ones specified by
35
49
  # {MU::Cloud}
36
50
  # @return [Array<Symbol>]
37
51
  def self.required_instance_methods
38
- []
52
+ [:url]
53
+ end
54
+
55
+ # A hook that is always called just before any of the instance method of
56
+ # our resource implementations gets invoked, so that we can ensure that
57
+ # repetitive setup tasks (like resolving +:resource_group+ for Azure
58
+ # resources) have always been done.
59
+ # @param cloudobj [MU::Cloud]
60
+ # @param deploy [MU::MommaCat]
61
+ def self.resourceInitHook(cloudobj, deploy)
62
+ class << self
63
+ attr_reader :project_id
64
+ attr_reader :customer
65
+ # url is too complex for an attribute (we get it from the cloud API),
66
+ # so it's up in AdditionalResourceMethods instead
67
+ end
68
+ return if !cloudobj
69
+
70
+ cloudobj.instance_variable_set(:@customer, MU::Cloud::Google.customerID(cloudobj.config['credentials']))
71
+
72
+ # XXX ensure @cloud_id and @project_id if this is a habitat
73
+ # XXX skip project_id if this is a folder or group
74
+ if deploy
75
+ # XXX this may be wrong for new deploys (but def right for regrooms)
76
+ project = MU::Cloud::Google.projectLookup(cloudobj.config['project'], deploy, sibling_only: true, raise_on_fail: false)
77
+ project_id = project.nil? ? cloudobj.config['project'] : project.cloudobj.cloud_id
78
+ cloudobj.instance_variable_set(:@project_id, project_id)
79
+ else
80
+ cloudobj.instance_variable_set(:@project_id, cloudobj.config['project'])
81
+ end
82
+
83
+ # XXX @url? Well we're not likely to have @cloud_desc at this point, so maybe
84
+ # that needs to be a generic-to-google wrapper like def url; cloud_desc.self_link;end
85
+
86
+ # XXX something like: vpc["habitat"] = MU::Cloud::Google.projectToRef(vpc["project"], config: configurator, credentials: vpc["credentials"])
39
87
  end
40
88
 
41
89
  # If we're running this cloud, return the $MU_CFG blob we'd use to
@@ -62,6 +110,17 @@ module MU
62
110
  sample
63
111
  end
64
112
 
113
+ # If we reside in this cloud, return the VPC in which we, the Mu Master, reside.
114
+ # @return [MU::Cloud::VPC]
115
+ def self.myVPCObj
116
+ return nil if !hosted?
117
+ instance = MU.myCloudDescriptor
118
+ return nil if !instance or !instance.network_interfaces or instance.network_interfaces.size == 0
119
+ vpc = MU::MommaCat.findStray("Google", "vpc", cloud_id: instance.network_interfaces.first.network.gsub(/.*?\/([^\/]+)$/, '\1'), dummy_ok: true, habitats: [myProject])
120
+ return nil if vpc.nil? or vpc.size == 0
121
+ vpc.first
122
+ end
123
+
65
124
  # Return the name strings of all known sets of credentials for this cloud
66
125
  # @return [Array<String>]
67
126
  def self.listCredentials
@@ -72,6 +131,88 @@ module MU
72
131
  $MU_CFG['google'].keys
73
132
  end
74
133
 
134
+ @@habmap = {}
135
+
136
+ # Return what we think of as a cloud object's habitat. In GCP, this means
137
+ # the +project_id+ in which is resident. If this is not applicable, such
138
+ # as for a {Habitat} or {Folder}, returns nil.
139
+ # @param cloudobj [MU::Cloud::Google]: The resource from which to extract the habitat id
140
+ # @return [String,nil]
141
+ def self.habitat(cloudobj, nolookup: false, deploy: nil)
142
+ @@habmap ||= {}
143
+ # XXX whaddabout config['habitat'] HNNNGH
144
+
145
+ return nil if !cloudobj.cloudclass.canLiveIn.include?(:Habitat)
146
+
147
+ # XXX users are assholes because they're valid two different ways ugh ugh
148
+ return nil if [MU::Cloud::Google::Group, MU::Cloud::Google::Folder].include?(cloudobj.cloudclass)
149
+ if cloudobj.config and cloudobj.config['project']
150
+ if nolookup
151
+ return cloudobj.config['project']
152
+ end
153
+ if @@habmap[cloudobj.config['project']]
154
+ return @@habmap[cloudobj.config['project']]
155
+ end
156
+ deploy ||= cloudobj.deploy if cloudobj.respond_to?(:deploy)
157
+
158
+ projectobj = projectLookup(cloudobj.config['project'], deploy, raise_on_fail: false)
159
+
160
+ if projectobj
161
+ @@habmap[cloudobj.config['project']] = projectobj.cloud_id
162
+ return projectobj.cloud_id
163
+ end
164
+ end
165
+
166
+ # blow up if this resource *has* to live in a project
167
+ if cloudobj.cloudclass.canLiveIn == [:Habitat]
168
+ MU.log "Failed to find project for cloudobj of class #{cloudobj.cloudclass.class.name}", MU::ERR, details: cloudobj
169
+ raise MuError, "Failed to find project for cloudobj of class #{cloudobj.cloudclass.class.name}"
170
+ end
171
+
172
+ nil
173
+ end
174
+
175
+ # Take a plain string that might be a reference to sibling project
176
+ # declared elsewhere in the active stack, or the project id of a live
177
+ # cloud resource, and return a {MU::Config::Ref} object
178
+ # @param project [String]: The name of a sibling project, or project id of an active project in GCP
179
+ # @param config [MU::Config]: A {MU::Config} object containing sibling resources, typically what we'd pass if we're calling during configuration parsing
180
+ # @param credentials [String]:
181
+ # @return [MU::Config::Ref]
182
+ def self.projectToRef(project, config: nil, credentials: nil)
183
+ return nil if !project
184
+
185
+ if config and config.haveLitterMate?(project, "habitat")
186
+ ref = MU::Config::Ref.new(
187
+ name: project,
188
+ cloud: "Google",
189
+ credentials: credentials,
190
+ type: "habitats"
191
+ )
192
+ end
193
+
194
+ if !ref
195
+ resp = MU::MommaCat.findStray(
196
+ "Google",
197
+ "habitats",
198
+ cloud_id: project,
199
+ credentials: credentials,
200
+ dummy_ok: true
201
+ )
202
+ if resp and resp.size > 0
203
+ project_obj = resp.first
204
+ ref = MU::Config::Ref.new(
205
+ id: project_obj.cloud_id,
206
+ cloud: "Google",
207
+ credentials: credentials,
208
+ type: "habitats"
209
+ )
210
+ end
211
+ end
212
+
213
+ ref
214
+ end
215
+
75
216
  # A shortcut for {MU::MommaCat.findStray} to resolve a shorthand project
76
217
  # name into a cloud object, whether it refers to a sibling by internal
77
218
  # name or by cloud identifier.
@@ -79,20 +220,21 @@ module MU
79
220
  # @param deploy [String]
80
221
  # @param raise_on_fail [Boolean]
81
222
  # @param sibling_only [Boolean]
82
- # @return [MU::Cloud::Habitat,nil]
223
+ # @return [MU::Config::Habitat,nil]
83
224
  def self.projectLookup(name, deploy = MU.mommacat, raise_on_fail: true, sibling_only: false)
84
- project_obj = deploy.findLitterMate(type: "habitats", name: name)
225
+ project_obj = deploy.findLitterMate(type: "habitats", name: name) if deploy
85
226
 
86
227
  if !project_obj and !sibling_only
87
228
  resp = MU::MommaCat.findStray(
88
229
  "Google",
89
230
  "habitats",
90
- deploy_id: deploy.deploy_id,
231
+ deploy_id: deploy ? deploy.deploy_id : nil,
91
232
  cloud_id: name,
92
233
  name: name,
93
234
  dummy_ok: true
94
235
  )
95
- project_obj = resp.first if resp
236
+
237
+ project_obj = resp.first if resp and resp.size > 0
96
238
  end
97
239
 
98
240
  if (!project_obj or !project_obj.cloud_id) and raise_on_fail
@@ -109,6 +251,9 @@ module MU
109
251
  def self.adminBucketName(credentials = nil)
110
252
  #XXX find a default if this particular account doesn't have a log_bucket_name configured
111
253
  cfg = credConfig(credentials)
254
+ if cfg.nil?
255
+ raise MuError, "Failed to load Google credential set #{credentials}"
256
+ end
112
257
  cfg['log_bucket_name']
113
258
  end
114
259
 
@@ -124,7 +269,7 @@ module MU
124
269
  # credentials. If no account name is specified, will return one flagged as
125
270
  # default. Returns nil if GCP is not configured. Throws an exception if
126
271
  # an account name is specified which does not exist.
127
- # @param name [String]: The name of the key under 'aws' in mu.yaml to return
272
+ # @param name [String]: The name of the key under 'google' in mu.yaml to return
128
273
  # @return [Hash,nil]
129
274
  def self.credConfig(name = nil, name_only: false)
130
275
  # If there's nothing in mu.yaml (which is wrong), but we're running
@@ -134,23 +279,17 @@ module MU
134
279
  return @@my_hosted_cfg if @@my_hosted_cfg
135
280
 
136
281
  if hosted?
137
- begin
138
- # iam_data = JSON.parse(getAWSMetaData("iam/info"))
139
- # if iam_data["InstanceProfileArn"] and !iam_data["InstanceProfileArn"].empty?
140
- @@my_hosted_cfg = hosted_config
141
- return name_only ? "#default" : @@my_hosted_cfg
142
- # end
143
- rescue JSON::ParserError => e
144
- end
282
+ @@my_hosted_cfg = hosted_config
283
+ return name_only ? "#default" : @@my_hosted_cfg
145
284
  end
146
285
 
147
286
  return nil
148
287
  end
149
288
 
150
289
  if name.nil?
151
- $MU_CFG['google'].each_pair { |name, cfg|
290
+ $MU_CFG['google'].each_pair { |set, cfg|
152
291
  if cfg['default']
153
- return name_only ? name : cfg
292
+ return name_only ? set : cfg
154
293
  end
155
294
  }
156
295
  else
@@ -173,10 +312,24 @@ module MU
173
312
  elsif MU::Cloud::Google.hosted?
174
313
  zone = MU::Cloud::Google.getGoogleMetaData("instance/zone")
175
314
  @@myRegion_var = zone.gsub(/^.*?\/|\-\d+$/, "")
315
+ else
316
+ @@myRegion_var = "us-east4"
176
317
  end
177
318
  @@myRegion_var
178
319
  end
179
320
 
321
+ # Do cloud-specific deploy instantiation tasks, such as copying SSH keys
322
+ # around, sticking secrets in buckets, creating resource groups, etc
323
+ # @param deploy [MU::MommaCat]
324
+ def self.initDeploy(deploy)
325
+ end
326
+
327
+ # Purge cloud-specific deploy meta-artifacts (SSH keys, resource groups,
328
+ # etc)
329
+ # @param deploy_id [MU::MommaCat]
330
+ def self.cleanDeploy(deploy_id, credentials: nil, noop: false)
331
+ end
332
+
180
333
  # Plant a Mu deploy secret into a storage bucket somewhere for so our kittens can consume it
181
334
  # @param deploy_id [String]: The deploy for which we're writing the secret
182
335
  # @param value [String]: The contents of the secret
@@ -250,9 +403,10 @@ module MU
250
403
  )
251
404
  }
252
405
  rescue ::Google::Apis::ClientError => e
406
+ MU.log e.message, MU::WARN, details: e.inspect
253
407
  if e.inspect.match(/body: "Not Found"/)
254
408
  raise MuError, "Google admin bucket #{adminBucketName(credentials)} or key #{name} does not appear to exist or is not visible with #{credentials ? credentials : "default"} credentials"
255
- elsif e.inspect.match(/notFound: No such object:/)
409
+ elsif e.message.match(/notFound: /)
256
410
  if retries < 5
257
411
  sleep 5
258
412
  retries += 1
@@ -265,7 +419,7 @@ module MU
265
419
  sleep 10
266
420
  retry
267
421
  else
268
- raise MuError, "Got #{e.inspect} trying to set ACLs for #{deploy_id} in #{adminBucketName(credentials)}"
422
+ raise MuError, "Got #{e.message} trying to set ACLs for #{deploy_id} in #{adminBucketName(credentials)}"
269
423
  end
270
424
  end
271
425
  end
@@ -289,7 +443,7 @@ module MU
289
443
  return @@is_in_gcp
290
444
  end
291
445
 
292
- if getGoogleMetaData("instance/name")
446
+ if getGoogleMetaData("project/project-id")
293
447
  @@is_in_gcp = true
294
448
  return true
295
449
  end
@@ -323,11 +477,11 @@ module MU
323
477
  # @param name [String]: A resource name for the certificate
324
478
  # @param cert [String,OpenSSL::X509::Certificate]: An x509 certificate
325
479
  # @param key [String,OpenSSL::PKey]: An x509 private key
326
- # @return [Google::Apis::ComputeBeta::SslCertificate]
480
+ # @return [Google::Apis::ComputeV1::SslCertificate]
327
481
  def self.createSSLCertificate(name, cert, key, flags = {}, credentials: nil)
328
482
  flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
329
483
  flags["description"] ||= MU.deploy_id
330
- certobj = ::Google::Apis::ComputeBeta::SslCertificate.new(
484
+ certobj = ::Google::Apis::ComputeV1::SslCertificate.new(
331
485
  name: name,
332
486
  certificate: cert.to_s,
333
487
  private_key: key.to_s,
@@ -354,27 +508,40 @@ module MU
354
508
 
355
509
  cfg = credConfig(credentials)
356
510
 
357
- if cfg['project']
358
- @@enable_semaphores[cfg['project']] ||= Mutex.new
359
- end
360
-
361
511
  if cfg
512
+ if cfg['project']
513
+ @@enable_semaphores[cfg['project']] ||= Mutex.new
514
+ end
362
515
  data = nil
363
516
  @@authorizers[credentials] ||= {}
364
517
 
365
- def self.get_machine_credentials(scopes)
518
+ def self.get_machine_credentials(scopes, credentials = nil)
366
519
  @@svc_account_name = MU::Cloud::Google.getGoogleMetaData("instance/service-accounts/default/email")
367
520
  MU.log "We are hosted in GCP, so I will attempt to use the service account #{@@svc_account_name} to make API requests.", MU::DEBUG
368
521
 
369
522
  @@authorizers[credentials][scopes.to_s] = ::Google::Auth.get_application_default(scopes)
370
523
  @@authorizers[credentials][scopes.to_s].fetch_access_token!
371
524
  @@default_project ||= MU::Cloud::Google.getGoogleMetaData("project/project-id")
525
+ begin
526
+ listRegions(credentials: credentials)
527
+ listInstanceTypes(credentials: credentials)
528
+ listProjects(credentials)
529
+ rescue ::Google::Apis::ClientError => e
530
+ MU.log "Found machine credentials #{@@svc_account_name}, but these don't appear to have sufficient permissions or scopes", MU::WARN, details: scopes
531
+ @@authorizers.delete(credentials)
532
+ return nil
533
+ end
372
534
  @@authorizers[credentials][scopes.to_s]
373
535
  end
374
536
 
375
- if cfg["credentials_file"]
537
+ if cfg["credentials_file"] or cfg["credentials_encoded"]
538
+
376
539
  begin
377
- data = JSON.parse(File.read(cfg["credentials_file"]))
540
+ data = if cfg["credentials_encoded"]
541
+ JSON.parse(Base64.decode64(cfg["credentials_encoded"]))
542
+ else
543
+ JSON.parse(File.read(cfg["credentials_file"]))
544
+ end
378
545
  @@default_project ||= data["project_id"]
379
546
  creds = {
380
547
  :json_key_io => StringIO.new(MultiJson.dump(data)),
@@ -388,18 +555,20 @@ module MU
388
555
  raise MuError, "Google Cloud credentials file #{cfg["credentials_file"]} is missing or invalid (#{e.message})"
389
556
  end
390
557
  MU.log "Google Cloud credentials file #{cfg["credentials_file"]} is missing or invalid", MU::WARN, details: e.message
391
- return get_machine_credentials(scopes)
558
+ return get_machine_credentials(scopes, credentials)
392
559
  end
393
560
  elsif cfg["credentials"]
394
561
  begin
395
562
  vault, item = cfg["credentials"].split(/:/)
396
563
  data = MU::Groomer::Chef.getSecret(vault: vault, item: item).to_h
397
- rescue MU::Groomer::Chef::MuNoSuchSecret
564
+ rescue MU::Groomer::MuNoSuchSecret
398
565
  if !MU::Cloud::Google.hosted?
399
566
  raise MuError, "Google Cloud credentials not found in Vault #{vault}:#{item}"
400
567
  end
401
568
  MU.log "Google Cloud credentials not found in Vault #{vault}:#{item}", MU::WARN
402
- return get_machine_credentials(scopes)
569
+ found = get_machine_credentials(scopes, credentials)
570
+ raise MuError, "No valid credentials available! Either grant admin privileges to machine service account, or manually add a different one with mu-configure" if found.nil?
571
+ return found
403
572
  end
404
573
 
405
574
  @@default_project ||= data["project_id"]
@@ -411,7 +580,9 @@ module MU
411
580
  @@authorizers[credentials][scopes.to_s] = ::Google::Auth::ServiceAccountCredentials.make_creds(creds)
412
581
  return @@authorizers[credentials][scopes.to_s]
413
582
  elsif MU::Cloud::Google.hosted?
414
- return get_machine_credentials(scopes)
583
+ found = get_machine_credentials(scopes, credentials)
584
+ raise MuError, "No valid credentials available! Either grant admin privileges to machine service account, or manually add a different one with mu-configure" if found.nil?
585
+ return found
415
586
  else
416
587
  raise MuError, "Google Cloud credentials not configured"
417
588
  end
@@ -455,15 +626,39 @@ module MU
455
626
  end
456
627
  end
457
628
 
629
+ @@default_project_cache = {}
630
+
458
631
  # Our credentials map to a project, an organizational structure in Google
459
632
  # Cloud. This fetches the identifier of the project associated with our
460
633
  # default credentials.
461
634
  # @param credentials [String]
462
635
  # @return [String]
463
636
  def self.defaultProject(credentials = nil)
637
+ if @@default_project_cache.has_key?(credentials)
638
+ return @@default_project_cache[credentials]
639
+ end
464
640
  cfg = credConfig(credentials)
465
- return myProject if !cfg or !cfg['project']
641
+ if !cfg or !cfg['project']
642
+ if hosted?
643
+ @@default_project_cache[credentials] = myProject
644
+ return myProject
645
+ end
646
+ if cfg
647
+ begin
648
+ result = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects
649
+ result.projects.reject! { |p| p.lifecycle_state == "DELETE_REQUESTED" }
650
+ available = result.projects.map { |p| p.project_id }
651
+ if available.size == 1
652
+ @@default_project_cache[credentials] = available[0]
653
+ return available[0]
654
+ end
655
+ rescue # fine
656
+ end
657
+ end
658
+ end
659
+ return nil if !cfg
466
660
  loadCredentials(credentials) if !@@authorizers[credentials]
661
+ @@default_project_cache[credentials] = cfg['project']
467
662
  cfg['project']
468
663
  end
469
664
 
@@ -486,7 +681,7 @@ module MU
486
681
  def self.listProjects(credentials = nil)
487
682
  cfg = credConfig(credentials)
488
683
  return [] if !cfg or !cfg['project']
489
- result = MU::Cloud::Google.resource_manager.list_projects
684
+ result = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects
490
685
  result.projects.reject! { |p| p.lifecycle_state == "DELETE_REQUESTED" }
491
686
  result.projects.map { |p| p.project_id }
492
687
  end
@@ -530,23 +725,28 @@ module MU
530
725
  # "translate" machine types across cloud providers.
531
726
  # @param region [String]: Supported machine types can vary from region to region, so we look for the set we're interested in specifically
532
727
  # @return [Hash]
533
- def self.listInstanceTypes(region = myRegion)
534
- return @@instance_types if @@instance_types and @@instance_types[region]
535
- if !MU::Cloud::Google.defaultProject
536
- return {}
728
+ def self.listInstanceTypes(region = self.myRegion, credentials: nil, project: MU::Cloud::Google.defaultProject)
729
+ return {} if !credConfig(credentials)
730
+ if @@instance_types and
731
+ @@instance_types[project] and
732
+ @@instance_types[project][region]
733
+ return @@instance_types
537
734
  end
538
735
 
736
+ return {} if !project
737
+
539
738
  @@instance_types ||= {}
540
- @@instance_types[region] ||= {}
541
- result = MU::Cloud::Google.compute.list_machine_types(MU::Cloud::Google.defaultProject, listAZs(region).first)
739
+ @@instance_types[project] ||= {}
740
+ @@instance_types[project][region] ||= {}
741
+ result = MU::Cloud::Google.compute(credentials: credentials).list_machine_types(project, listAZs(region).first)
542
742
  result.items.each { |type|
543
- @@instance_types[region][type.name] ||= {}
544
- @@instance_types[region][type.name]["memory"] = sprintf("%.1f", type.memory_mb/1024.0).to_f
545
- @@instance_types[region][type.name]["vcpu"] = type.guest_cpus.to_f
743
+ @@instance_types[project][region][type.name] ||= {}
744
+ @@instance_types[project][region][type.name]["memory"] = sprintf("%.1f", type.memory_mb/1024.0).to_f
745
+ @@instance_types[project][region][type.name]["vcpu"] = type.guest_cpus.to_f
546
746
  if type.is_shared_cpu
547
- @@instance_types[region][type.name]["ecu"] = "Variable"
747
+ @@instance_types[project][region][type.name]["ecu"] = "Variable"
548
748
  else
549
- @@instance_types[region][type.name]["ecu"] = type.guest_cpus
749
+ @@instance_types[project][region][type.name]["ecu"] = type.guest_cpus
550
750
  end
551
751
  }
552
752
  @@instance_types
@@ -563,22 +763,26 @@ module MU
563
763
  # server resides (if it resides in this cloud provider's ecosystem).
564
764
  # @param region [String]: The region to search.
565
765
  # @return [Array<String>]: The Availability Zones in this region.
566
- def self.listAZs(region = MU.curRegion)
766
+ def self.listAZs(region = self.myRegion)
767
+ return [] if !credConfig
567
768
  MU::Cloud::Google.listRegions if !@@regions.has_key?(region)
568
- raise MuError, "No such Google Cloud region '#{region}'" if !@@regions.has_key?(region)
769
+ if !@@regions.has_key?(region)
770
+ MU.log "Failed to get GCP region #{region}", MU::ERR, details: @@regions
771
+ raise MuError, "No such Google Cloud region '#{region}'" if !@@regions.has_key?(region)
772
+ end
569
773
  @@regions[region]
570
774
  end
571
775
 
572
776
  # Google's Compute Service API
573
- # @param subclass [<Google::Apis::ComputeBeta>]: If specified, will return the class ::Google::Apis::ComputeBeta::subclass instead of an API client instance
777
+ # @param subclass [<Google::Apis::ComputeV1>]: If specified, will return the class ::Google::Apis::ComputeV1::subclass instead of an API client instance
574
778
  def self.compute(subclass = nil, credentials: nil)
575
- require 'google/apis/compute_beta'
779
+ require 'google/apis/compute_v1'
576
780
 
577
781
  if subclass.nil?
578
- @@compute_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], credentials: credentials)
782
+ @@compute_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ComputeV1::ComputeService", scopes: ['cloud-platform', 'compute.readonly'], credentials: credentials)
579
783
  return @@compute_api[credentials]
580
784
  elsif subclass.is_a?(Symbol)
581
- return Object.const_get("::Google").const_get("Apis").const_get("ComputeBeta").const_get(subclass)
785
+ return Object.const_get("::Google").const_get("Apis").const_get("ComputeV1").const_get(subclass)
582
786
  end
583
787
  end
584
788
 
@@ -588,7 +792,7 @@ module MU
588
792
  require 'google/apis/storage_v1'
589
793
 
590
794
  if subclass.nil?
591
- @@storage_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "StorageV1::StorageService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
795
+ @@storage_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "StorageV1::StorageService", scopes: ['cloud-platform'], credentials: credentials)
592
796
  return @@storage_api[credentials]
593
797
  elsif subclass.is_a?(Symbol)
594
798
  return Object.const_get("::Google").const_get("Apis").const_get("StorageV1").const_get(subclass)
@@ -601,7 +805,7 @@ module MU
601
805
  require 'google/apis/iam_v1'
602
806
 
603
807
  if subclass.nil?
604
- @@iam_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "IamV1::IamService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
808
+ @@iam_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "IamV1::IamService", scopes: ['cloud-platform', 'cloudplatformprojects', 'cloudplatformorganizations', 'cloudplatformfolders'], credentials: credentials)
605
809
  return @@iam_api[credentials]
606
810
  elsif subclass.is_a?(Symbol)
607
811
  return Object.const_get("::Google").const_get("Apis").const_get("IamV1").const_get(subclass)
@@ -612,18 +816,29 @@ module MU
612
816
  # @param subclass [<Google::Apis::AdminDirectoryV1>]: If specified, will return the class ::Google::Apis::AdminDirectoryV1::subclass instead of an API client instance
613
817
  def self.admin_directory(subclass = nil, credentials: nil)
614
818
  require 'google/apis/admin_directory_v1'
615
-
616
- if subclass.nil?
617
- begin
618
- @@admin_directory_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "AdminDirectoryV1::DirectoryService", scopes: ['https://www.googleapis.com/auth/admin.directory.group.member.readonly', 'https://www.googleapis.com/auth/admin.directory.group.readonly', 'https://www.googleapis.com/auth/admin.directory.user.readonly', 'https://www.googleapis.com/auth/admin.directory.domain.readonly', 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly', 'https://www.googleapis.com/auth/admin.directory.customer.readonly'], masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'], credentials: credentials)
619
- rescue Signet::AuthorizationError => e
620
- MU.log "Cannot masquerade as #{MU::Cloud::Google.credConfig(credentials)['masquerade_as']}", MU::ERROR, details: "You can only use masquerade_as with GSuite. For more information on delegating GSuite authority to a service account, see:\nhttps://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority"
621
- raise e
819
+
820
+ writescopes = ['admin.directory.group.member', 'admin.directory.group', 'admin.directory.user', 'admin.directory.domain', 'admin.directory.orgunit', 'admin.directory.rolemanagement', 'admin.directory.customer', 'admin.directory.user.alias', 'admin.directory.userschema']
821
+ readscopes = ['admin.directory.group.member.readonly', 'admin.directory.group.readonly', 'admin.directory.user.readonly', 'admin.directory.domain.readonly', 'admin.directory.orgunit.readonly', 'admin.directory.rolemanagement.readonly', 'admin.directory.customer.readonly', 'admin.directory.user.alias.readonly', 'admin.directory.userschema.readonly']
822
+ @@readonly_semaphore.synchronize {
823
+ use_scopes = readscopes+writescopes
824
+ if @@readonly[credentials] and @@readonly[credentials]["AdminDirectoryV1"]
825
+ use_scopes = readscopes.dup
622
826
  end
623
- return @@admin_directory_api[credentials]
624
- elsif subclass.is_a?(Symbol)
625
- return Object.const_get("::Google").const_get("Apis").const_get("AdminDirectoryV1").const_get(subclass)
626
- end
827
+
828
+ if subclass.nil?
829
+ begin
830
+ @@admin_directory_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "AdminDirectoryV1::DirectoryService", scopes: use_scopes, masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'], credentials: credentials)
831
+ rescue Signet::AuthorizationError => e
832
+ MU.log "Falling back to read-only access to DirectoryService API for credential set '#{credentials}'", MU::WARN
833
+ @@admin_directory_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "AdminDirectoryV1::DirectoryService", scopes: readscopes, masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'], credentials: credentials)
834
+ @@readonly[credentials] ||= {}
835
+ @@readonly[credentials]["AdminDirectoryV1"] = true
836
+ end
837
+ return @@admin_directory_api[credentials]
838
+ elsif subclass.is_a?(Symbol)
839
+ return Object.const_get("::Google").const_get("Apis").const_get("AdminDirectoryV1").const_get(subclass)
840
+ end
841
+ }
627
842
  end
628
843
 
629
844
  # Google's Cloud Resource Manager API
@@ -632,8 +847,10 @@ module MU
632
847
  require 'google/apis/cloudresourcemanager_v1'
633
848
 
634
849
  if subclass.nil?
635
- # @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformprojects'], masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'], credentials: credentials)
636
- @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformprojects'], credentials: credentials)
850
+ if !MU::Cloud::Google.credConfig(credentials)
851
+ raise MuError, "No such credential set #{credentials} defined in mu.yaml!"
852
+ end
853
+ @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['cloud-platform', 'cloudplatformprojects', 'cloudplatformorganizations', 'cloudplatformfolders'], credentials: credentials, masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'])
637
854
  return @@resource_api[credentials]
638
855
  elsif subclass.is_a?(Symbol)
639
856
  return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV1").const_get(subclass)
@@ -646,7 +863,7 @@ module MU
646
863
  require 'google/apis/cloudresourcemanager_v2'
647
864
 
648
865
  if subclass.nil?
649
- @@resource2_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV2::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformfolders'], credentials: credentials)
866
+ @@resource2_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV2::CloudResourceManagerService", scopes: ['cloud-platform', 'cloudplatformfolders'], credentials: credentials, masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'])
650
867
  return @@resource2_api[credentials]
651
868
  elsif subclass.is_a?(Symbol)
652
869
  return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV2").const_get(subclass)
@@ -659,7 +876,7 @@ module MU
659
876
  require 'google/apis/container_v1'
660
877
 
661
878
  if subclass.nil?
662
- @@container_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ContainerV1::ContainerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
879
+ @@container_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ContainerV1::ContainerService", scopes: ['cloud-platform'], credentials: credentials)
663
880
  return @@container_api[credentials]
664
881
  elsif subclass.is_a?(Symbol)
665
882
  return Object.const_get("::Google").const_get("Apis").const_get("ContainerV1").const_get(subclass)
@@ -672,7 +889,7 @@ module MU
672
889
  require 'google/apis/servicemanagement_v1'
673
890
 
674
891
  if subclass.nil?
675
- @@service_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ServicemanagementV1::ServiceManagementService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
892
+ @@service_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "ServicemanagementV1::ServiceManagementService", scopes: ['cloud-platform'], credentials: credentials)
676
893
  return @@service_api[credentials]
677
894
  elsif subclass.is_a?(Symbol)
678
895
  return Object.const_get("::Google").const_get("Apis").const_get("ServicemanagementV1").const_get(subclass)
@@ -685,7 +902,7 @@ module MU
685
902
  require 'google/apis/sqladmin_v1beta4'
686
903
 
687
904
  if subclass.nil?
688
- @@sql_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "SqladminV1beta4::SQLAdminService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
905
+ @@sql_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "SqladminV1beta4::SQLAdminService", scopes: ['cloud-platform'], credentials: credentials)
689
906
  return @@sql_api[credentials]
690
907
  elsif subclass.is_a?(Symbol)
691
908
  return Object.const_get("::Google").const_get("Apis").const_get("SqladminV1beta4").const_get(subclass)
@@ -698,7 +915,7 @@ module MU
698
915
  require 'google/apis/firestore_v1'
699
916
 
700
917
  if subclass.nil?
701
- @@firestore_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "FirestoreV1::FirestoreService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
918
+ @@firestore_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "FirestoreV1::FirestoreService", scopes: ['cloud-platform'], credentials: credentials)
702
919
  return @@firestore_api[credentials]
703
920
  elsif subclass.is_a?(Symbol)
704
921
  return Object.const_get("::Google").const_get("Apis").const_get("FirestoreV1").const_get(subclass)
@@ -711,7 +928,7 @@ module MU
711
928
  require 'google/apis/logging_v2'
712
929
 
713
930
  if subclass.nil?
714
- @@logging_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "LoggingV2::LoggingService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
931
+ @@logging_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "LoggingV2::LoggingService", scopes: ['cloud-platform'], credentials: credentials)
715
932
  return @@logging_api[credentials]
716
933
  elsif subclass.is_a?(Symbol)
717
934
  return Object.const_get("::Google").const_get("Apis").const_get("LoggingV2").const_get(subclass)
@@ -724,26 +941,86 @@ module MU
724
941
  require 'google/apis/cloudbilling_v1'
725
942
 
726
943
  if subclass.nil?
727
- @@billing_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudbillingV1::CloudbillingService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
944
+ @@billing_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudbillingV1::CloudbillingService", scopes: ['cloud-platform', 'cloud-billing'], credentials: credentials, masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'])
728
945
  return @@billing_api[credentials]
729
946
  elsif subclass.is_a?(Symbol)
730
947
  return Object.const_get("::Google").const_get("Apis").const_get("CloudbillingV1").const_get(subclass)
731
948
  end
732
949
  end
733
950
 
951
+ # Retrieve the domains, if any, which these credentials can manage via
952
+ # GSuite or Cloud Identity.
953
+ # @param credentials [String]
954
+ # @return [Array<String>],nil]
955
+ def self.getDomains(credentials = nil)
956
+ my_org = getOrg(credentials)
957
+ return nil if !my_org
958
+
959
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_domains(MU::Cloud::Google.customerID(credentials))
960
+ resp.domains.map { |d| d.domain_name.downcase }
961
+ end
734
962
 
963
+ @@orgmap = {}
735
964
  # Retrieve the organization, if any, to which these credentials belong.
736
965
  # @param credentials [String]
737
966
  # @return [Array<OpenStruct>],nil]
738
- def self.getOrg(credentials = nil)
739
- resp = MU::Cloud::Google.resource_manager(credentials: credentials).search_organizations
967
+ def self.getOrg(credentials = nil, with_id: nil)
968
+ creds = MU::Cloud::Google.credConfig(credentials)
969
+ credname = if creds and creds['name']
970
+ creds['name']
971
+ else
972
+ "default"
973
+ end
974
+
975
+ return @@orgmap[credname] if @@orgmap.has_key?(credname)
976
+ resp = MU::Cloud::Google.resource_manager(credentials: credname).search_organizations
740
977
  if resp and resp.organizations
741
978
  # XXX no idea if it's possible to be a member of multiple orgs
742
- return resp.organizations.first
979
+ if !with_id
980
+ @@orgmap[credname] = resp.organizations.first
981
+ return resp.organizations.first
982
+ else
983
+ resp.organizations.each { |org|
984
+ if org.name == with_id
985
+ @@orgmap[credname] = org
986
+ return org
987
+ end
988
+ }
989
+ return nil
990
+ end
743
991
  end
992
+
993
+ @@orgmap[credname] = nil
994
+
995
+
996
+ MU.log "Unable to list_organizations with credentials #{credname}. If this account is part of a GSuite or Cloud Identity domain, verify that Oauth delegation is properly configured and that 'masquerade_as' is properly set for the #{credname} Google credential set in mu.yaml.", MU::ERR, details: ["https://cloud.google.com/resource-manager/docs/creating-managing-organization", "https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients"]
997
+
744
998
  nil
745
999
  end
746
1000
 
1001
+ @@customer_ids_cache = {}
1002
+
1003
+ # Fetch the GSuite/Cloud Identity customer id for the domain associated
1004
+ # with the given credentials, if a domain is set via the +masquerade_as+
1005
+ # configuration option.
1006
+ def self.customerID(credentials = nil)
1007
+ cfg = credConfig(credentials)
1008
+ if !cfg or !cfg['masquerade_as']
1009
+ return nil
1010
+ end
1011
+
1012
+ if @@customer_ids_cache[credentials]
1013
+ return @@customer_ids_cache[credentials]
1014
+ end
1015
+
1016
+ user = MU::Cloud::Google.admin_directory(credentials: credentials).get_user(cfg['masquerade_as'])
1017
+ if user and user.customer_id
1018
+ @@customer_ids_cache[credentials] = user.customer_id
1019
+ end
1020
+
1021
+ @@customer_ids_cache[credentials]
1022
+ end
1023
+
747
1024
  private
748
1025
 
749
1026
  # Wrapper class for Google APIs, so that we can catch some common
@@ -752,18 +1029,37 @@ module MU
752
1029
  class GoogleEndpoint
753
1030
  @api = nil
754
1031
  @credentials = nil
1032
+ @scopes = nil
1033
+ @masquerade = nil
755
1034
  attr_reader :issuer
756
1035
 
757
1036
  # Create a Google Cloud Platform API client
758
1037
  # @param api [String]: Which API are we wrapping?
759
1038
  # @param scopes [Array<String>]: Google auth scopes applicable to this API
760
- def initialize(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], masquerade: nil, credentials: nil)
1039
+ def initialize(api: "ComputeV1::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], masquerade: nil, credentials: nil)
761
1040
  @credentials = credentials
1041
+ @scopes = scopes.map { |s|
1042
+ if !s.match(/\//) # allow callers to use shorthand
1043
+ s = "https://www.googleapis.com/auth/"+s
1044
+ end
1045
+ s
1046
+ }
1047
+ @masquerade = masquerade
762
1048
  @api = Object.const_get("Google::Apis::#{api}").new
763
- @api.authorization = MU::Cloud::Google.loadCredentials(scopes, credentials: credentials)
764
- if masquerade
765
- @api.authorization.sub = masquerade
766
- @api.authorization.fetch_access_token!
1049
+ @api.authorization = MU::Cloud::Google.loadCredentials(@scopes, credentials: credentials)
1050
+ raise MuError, "No useable Google credentials found#{credentials ? " with set '#{credentials}'" : ""}" if @api.authorization.nil?
1051
+ if @masquerade
1052
+ begin
1053
+ @api.authorization.sub = @masquerade
1054
+ @api.authorization.fetch_access_token!
1055
+ rescue Signet::AuthorizationError => e
1056
+ MU.log "Cannot masquerade as #{@masquerade} to API #{api}: #{e.message}", MU::ERROR, details: @scopes
1057
+ if e.message.match(/client not authorized for any of the scopes requested/)
1058
+ # XXX it'd be helpful to list *all* scopes we like, as well as the API client's numeric id
1059
+ MU.log "To grant access to API scopes for this service account, see:", MU::ERR, details: "https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients"
1060
+ end
1061
+ raise e
1062
+ end
767
1063
  end
768
1064
  @issuer = @api.authorization.issuer
769
1065
  end
@@ -780,9 +1076,9 @@ module MU
780
1076
  resp = nil
781
1077
  begin
782
1078
  if region
783
- resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, region, filter: filter)
1079
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, region, filter: filter, mu_gcp_enable_apis: false)
784
1080
  else
785
- resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, filter: filter)
1081
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, filter: filter, mu_gcp_enable_apis: false)
786
1082
  end
787
1083
 
788
1084
  rescue ::Google::Apis::ClientError => e
@@ -795,6 +1091,7 @@ module MU
795
1091
  resp.items.each { |obj|
796
1092
  threads << Thread.new {
797
1093
  MU.dupGlobals(parent_thread_id)
1094
+ Thread.abort_on_exception = false
798
1095
  MU.log "Removing #{type.gsub(/_/, " ")} #{obj.name}"
799
1096
  delete_sym = "delete_#{type}".to_sym
800
1097
  if !noop
@@ -813,9 +1110,10 @@ module MU
813
1110
  failed = true
814
1111
  retries += 1
815
1112
  if resp.error.errors.first.code == "RESOURCE_IN_USE_BY_ANOTHER_RESOURCE" and retries < 6
816
- sleep 15
1113
+ sleep 10
817
1114
  else
818
1115
  MU.log "Error deleting #{type.gsub(/_/, " ")} #{obj.name}", MU::ERR, details: resp.error.errors
1116
+ Thread.abort_on_exception = false
819
1117
  raise MuError, "Failed to delete #{type.gsub(/_/, " ")} #{obj.name}"
820
1118
  end
821
1119
  else
@@ -824,6 +1122,8 @@ module MU
824
1122
  # TODO validate that the resource actually went away, because it seems not to do so very reliably
825
1123
  rescue ::Google::Apis::ClientError => e
826
1124
  raise e if !e.message.match(/(^notFound: |operation in progress)/)
1125
+ rescue MU::Cloud::MuDefunctHabitat => e
1126
+ # this is ok- it's already deleted
827
1127
  end while failed and retries < 6
828
1128
  end
829
1129
  }
@@ -841,10 +1141,31 @@ module MU
841
1141
  def method_missing(method_sym, *arguments)
842
1142
  retries = 0
843
1143
  actual_resource = nil
1144
+
1145
+ enable_on_fail = true
1146
+ arguments.each { |arg|
1147
+ if arg.is_a?(Hash) and arg.has_key?(:mu_gcp_enable_apis)
1148
+ enable_on_fail = arg[:mu_gcp_enable_apis]
1149
+ arg.delete(:mu_gcp_enable_apis)
1150
+
1151
+ end
1152
+ }
1153
+ arguments.delete({})
1154
+ next_page_token = nil
1155
+ overall_retval = nil
1156
+
844
1157
  begin
845
1158
  MU.log "Calling #{method_sym}", MU::DEBUG, details: arguments
846
1159
  retval = nil
847
1160
  retries = 0
1161
+ wait_backoff = 5
1162
+ if next_page_token
1163
+ if arguments.size == 1 and arguments.first.is_a?(Hash)
1164
+ arguments[0][:page_token] = next_page_token
1165
+ else
1166
+ arguments << { :page_token => next_page_token }
1167
+ end
1168
+ end
848
1169
  begin
849
1170
  if !arguments.nil? and arguments.size == 1
850
1171
  retval = @api.method(method_sym).call(arguments[0])
@@ -853,49 +1174,93 @@ module MU
853
1174
  else
854
1175
  retval = @api.method(method_sym).call
855
1176
  end
1177
+ rescue ArgumentError => e
1178
+ MU.log "#{e.class.name} calling #{@api.class.name}.#{method_sym.to_s}: #{e.message}", MU::ERR, details: arguments
1179
+ raise e
856
1180
  rescue ::Google::Apis::AuthorizationError => e
857
1181
  if arguments.size > 0
858
1182
  raise MU::MuError, "Service account #{MU::Cloud::Google.svc_account_name} has insufficient privileges to call #{method_sym} in project #{arguments.first}"
859
1183
  else
860
1184
  raise MU::MuError, "Service account #{MU::Cloud::Google.svc_account_name} has insufficient privileges to call #{method_sym}"
861
1185
  end
1186
+ rescue ::Google::Apis::RateLimitError, ::Google::Apis::TransmissionError, ::ThreadError, ::Google::Apis::ServerError => e
1187
+ if retries <= 10
1188
+ sleep wait_backoff
1189
+ retries += 1
1190
+ wait_backoff = wait_backoff * 2
1191
+ retry
1192
+ else
1193
+ raise e
1194
+ end
862
1195
  rescue ::Google::Apis::ClientError, OpenSSL::SSL::SSLError => e
863
- if e.message.match(/^invalidParameter:/)
864
- MU.log "#{method_sym.to_s}: "+e.message, MU::ERR, details: arguments
1196
+ if e.message.match(/^quotaExceeded: Request rate/)
1197
+ if retries <= 10
1198
+ sleep wait_backoff
1199
+ retries += 1
1200
+ wait_backoff = wait_backoff * 2
1201
+ retry
1202
+ else
1203
+ raise e
1204
+ end
1205
+ elsif e.message.match(/^invalidParameter:|^badRequest:/)
1206
+ MU.log "#{e.class.name} calling #{@api.class.name}.#{method_sym.to_s}: "+e.message, MU::ERR, details: arguments
865
1207
  # uncomment for debugging stuff; this can occur in benign situations so we don't normally want it logging
866
1208
  elsif e.message.match(/^forbidden:/)
867
- MU.log "Using credentials #{@credentials}: #{method_sym.to_s}: "+e.message, MU::ERR, details: caller
1209
+ MU.log "#{e.class.name} calling #{@api.class.name}.#{method_sym.to_s} got \"#{e.message}\" using credentials #{@credentials}#{@masquerade ? " (OAuth'd as #{@masquerade})": ""}.#{@scopes ? "\nScopes:\n#{@scopes.join("\n")}" : "" }", MU::DEBUG, details: arguments
1210
+ raise e
868
1211
  end
869
1212
  @@enable_semaphores ||= {}
870
1213
  max_retries = 3
871
1214
  wait_time = 90
872
- if retries <= max_retries and e.message.match(/^accessNotConfigured/)
1215
+ if enable_on_fail and retries <= max_retries and e.message.match(/^accessNotConfigured/)
873
1216
  enable_obj = nil
874
- project = arguments.size > 0 ? arguments.first.to_s : MU::Cloud::Google.defaultProject(@credentials)
1217
+
1218
+ project = if arguments.size > 0 and arguments.first.is_a?(String)
1219
+ arguments.first
1220
+ else
1221
+ MU::Cloud::Google.defaultProject(@credentials)
1222
+ end
1223
+ # XXX validate that this actually looks like a project id, maybe
1224
+ if method_sym == :delete and !MU::Cloud::Google::Habitat.isLive?(project, @credentials)
1225
+ MU.log "Got accessNotConfigured while attempting to delete a resource in #{project}", MU::WARN
1226
+
1227
+ return
1228
+ end
1229
+
875
1230
  @@enable_semaphores[project] ||= Mutex.new
876
1231
  enable_obj = MU::Cloud::Google.service_manager(:EnableServiceRequest).new(
877
1232
  consumer_id: "project:"+project
878
1233
  )
879
1234
  # XXX dumbass way to get this string
880
- e.message.match(/by visiting https:\/\/console\.developers\.google\.com\/apis\/api\/(.+?)\//)
881
-
882
- svc_name = Regexp.last_match[1]
883
- save_verbosity = MU.verbosity
884
- if svc_name != "servicemanagement.googleapis.com"
885
- retries += 1
886
- @@enable_semaphores[project].synchronize {
1235
+ if e.message.match(/by visiting https:\/\/console\.developers\.google\.com\/apis\/api\/(.+?)\//)
1236
+
1237
+ svc_name = Regexp.last_match[1]
1238
+ save_verbosity = MU.verbosity
1239
+ if svc_name != "servicemanagement.googleapis.com" and method_sym != :delete
1240
+ retries += 1
1241
+ @@enable_semaphores[project].synchronize {
1242
+ MU.setLogging(MU::Logger::NORMAL)
1243
+ MU.log "Attempting to enable #{svc_name} in project #{project}; will retry #{method_sym.to_s} in #{(wait_time/retries).to_s}s (#{retries.to_s}/#{max_retries.to_s})", MU::NOTICE
1244
+ MU.setLogging(save_verbosity)
1245
+ begin
1246
+ MU::Cloud::Google.service_manager(credentials: @credentials).enable_service(svc_name, enable_obj)
1247
+ rescue ::Google::Apis::ClientError => e
1248
+ MU.log "Error enabling #{svc_name} in #{project} for #{method_sym.to_s}: "+ e.message, MU::ERR, details: enable_obj
1249
+ raise e
1250
+ end
1251
+ }
1252
+ sleep wait_time/retries
1253
+ retry
1254
+ else
887
1255
  MU.setLogging(MU::Logger::NORMAL)
888
- MU.log "Attempting to enable #{svc_name} in project #{project}; will retry #{method_sym.to_s} in #{(wait_time/retries).to_s}s (#{retries.to_s}/#{max_retries.to_s})", MU::NOTICE
1256
+ MU.log "Google Cloud's Service Management API must be enabled manually by visiting #{e.message.gsub(/.*?(https?:\/\/[^\s]+)(?:$|\s).*/, '\1')}", MU::ERR
889
1257
  MU.setLogging(save_verbosity)
890
- MU::Cloud::Google.service_manager(credentials: @credentials).enable_service(svc_name, enable_obj)
891
- }
892
- sleep wait_time/retries
893
- retry
1258
+ raise MU::MuError, "Service Management API not yet enabled for this account/project"
1259
+ end
1260
+ elsif e.message.match(/scheduled for deletion and cannot be used for API calls/)
1261
+ raise MuDefunctHabitat, e.message
894
1262
  else
895
- MU.setLogging(MU::Logger::NORMAL)
896
- MU.log "Google Cloud's Service Management API must be enabled manually by visiting #{e.message.gsub(/.*?(https?:\/\/[^\s]+)(?:$|\s).*/, '\1')}", MU::ERR
897
- MU.setLogging(save_verbosity)
898
- raise MU::MuError, "Service Management API not yet enabled for this account/project"
1263
+ MU.log "Unfamiliar error calling #{method_sym.to_s} "+e.message, MU::ERR, details: arguments
899
1264
  end
900
1265
  elsif retries <= 10 and
901
1266
  e.message.match(/^resourceNotReady:/) or
@@ -914,9 +1279,17 @@ module MU
914
1279
  end
915
1280
  end
916
1281
 
917
- if retval.class == ::Google::Apis::ComputeBeta::Operation
1282
+ if retval.class.name.match(/.*?::Operation$/)
1283
+
918
1284
  retries = 0
919
1285
  orig_target = retval.name
1286
+
1287
+ # Check whether the various types of +Operation+ responses say
1288
+ # they're done, without knowing which specific API they're from
1289
+ def is_done?(retval)
1290
+ (retval.respond_to?(:status) and retval.status == "DONE") or (retval.respond_to?(:done) and retval.done)
1291
+ end
1292
+
920
1293
  begin
921
1294
  if retries > 0 and retries % 3 == 0
922
1295
  MU.log "Waiting for #{method_sym} to be done (retry #{retries})", MU::NOTICE
@@ -924,14 +1297,37 @@ module MU
924
1297
  MU.log "Waiting for #{method_sym} to be done (retry #{retries})", MU::DEBUG, details: retval
925
1298
  end
926
1299
 
927
- if retval.status != "DONE"
1300
+ if !is_done?(retval)
928
1301
  sleep 7
929
1302
  begin
930
- resp = MU::Cloud::Google.compute(credentials: @credentials).get_global_operation(
931
- arguments.first, # there's always a project id
932
- retval.name
933
- )
934
- retval = resp
1303
+ if retval.class.name.match(/::Compute[^:]*::/)
1304
+ resp = MU::Cloud::Google.compute(credentials: @credentials).get_global_operation(
1305
+ arguments.first, # there's always a project id
1306
+ retval.name
1307
+ )
1308
+ retval = resp
1309
+ elsif retval.class.name.match(/::Servicemanagement[^:]*::/)
1310
+ resp = MU::Cloud::Google.service_manager(credentials: @credentials).get_operation(
1311
+ retval.name
1312
+ )
1313
+ retval = resp
1314
+ elsif retval.class.name.match(/::Cloudresourcemanager[^:]*::/)
1315
+ resp = MU::Cloud::Google.resource_manager(credentials: @credentials).get_operation(
1316
+ retval.name
1317
+ )
1318
+ retval = resp
1319
+ if retval.error
1320
+ raise MuError, retval.error.message
1321
+ end
1322
+ elsif retval.class.name.match(/::Container[^:]*::/)
1323
+ resp = MU::Cloud::Google.container(credentials: @credentials).get_project_location_operation(
1324
+ retval.self_link.sub(/.*?\/projects\//, 'projects/')
1325
+ )
1326
+ retval = resp
1327
+ else
1328
+ pp retval
1329
+ raise MuError, "I NEED TO IMPLEMENT AN OPERATION HANDLER FOR #{retval.class.name}"
1330
+ end
935
1331
  rescue ::Google::Apis::ClientError => e
936
1332
  # this is ok; just means the operation is done and went away
937
1333
  if e.message.match(/^notFound:/)
@@ -942,7 +1338,8 @@ module MU
942
1338
  end
943
1339
  retries = retries + 1
944
1340
  end
945
- end while retval.status != "DONE"
1341
+
1342
+ end while !is_done?(retval)
946
1343
 
947
1344
  # Most insert methods have a predictable get_* counterpart. Let's
948
1345
  # take advantage.
@@ -960,6 +1357,10 @@ module MU
960
1357
  faked_args.pop
961
1358
  end
962
1359
  faked_args.push(cloud_id)
1360
+ if get_method == :get_project_location_cluster
1361
+ faked_args[0] = faked_args[0]+"/clusters/"+faked_args[1]
1362
+ faked_args.pop
1363
+ end
963
1364
  actual_resource = @api.method(get_method).call(*faked_args)
964
1365
  #if method_sym == :insert_instance
965
1366
  #MU.log "actual_resource", MU::WARN, details: actual_resource
@@ -982,7 +1383,43 @@ module MU
982
1383
  return actual_resource
983
1384
  end
984
1385
  end
985
- return retval
1386
+
1387
+ # This atrocity appends the pages of list_* results
1388
+ if overall_retval
1389
+ if method_sym.to_s.match(/^list_(.*)/)
1390
+ require 'google/apis/iam_v1'
1391
+ what = Regexp.last_match[1].to_sym
1392
+ whatassign = (Regexp.last_match[1]+"=").to_sym
1393
+ if overall_retval.class == ::Google::Apis::IamV1::ListServiceAccountsResponse
1394
+ what = :accounts
1395
+ whatassign = :accounts=
1396
+ end
1397
+ if retval.respond_to?(what) and retval.respond_to?(whatassign)
1398
+ if !retval.public_send(what).nil?
1399
+ newarray = retval.public_send(what) + overall_retval.public_send(what)
1400
+ overall_retval.public_send(whatassign, newarray)
1401
+ end
1402
+ else
1403
+ MU.log "Not sure how to append #{method_sym.to_s} results to #{overall_retval.class.name} (apparently #{what.to_s} and #{whatassign.to_s} aren't it), returning first page only", MU::WARN, details: retval
1404
+ return retval
1405
+ end
1406
+ else
1407
+ MU.log "Not sure how to append #{method_sym.to_s} results, returning first page only", MU::WARN, details: retval
1408
+ return retval
1409
+ end
1410
+ else
1411
+ overall_retval = retval
1412
+ end
1413
+
1414
+ arguments.delete({ :page_token => next_page_token })
1415
+ next_page_token = nil
1416
+
1417
+ if retval.respond_to?(:next_page_token) and !retval.next_page_token.nil?
1418
+ next_page_token = retval.next_page_token
1419
+ MU.log "Getting another page of #{method_sym.to_s}", MU::DEBUG, details: next_page_token
1420
+ else
1421
+ return overall_retval
1422
+ end
986
1423
  rescue ::Google::Apis::ServerError, ::Google::Apis::ClientError, ::Google::Apis::TransmissionError => e
987
1424
  if e.class.name == "Google::Apis::ClientError" and
988
1425
  (!method_sym.to_s.match(/^insert_/) or !e.message.match(/^notFound: /) or
@@ -996,8 +1433,8 @@ module MU
996
1433
  logs = MU::Cloud::Google.logging(credentials: @credentials).list_entry_log_entries(logreq)
997
1434
  details = nil
998
1435
  if logs.entries
999
- details = logs.entries.map { |e| e.json_payload }
1000
- details.reject! { |e| e["error"].nil? or e["error"].size == 0 }
1436
+ details = logs.entries.map { |err| err.json_payload }
1437
+ details.reject! { |err| err["error"].nil? or err["error"].size == 0 }
1001
1438
  end
1002
1439
 
1003
1440
  raise MuError, "#{method_sym.to_s} of #{retval.target_id} appeared to succeed, but then the resource disappeared! #{details.to_s}"
@@ -1022,7 +1459,7 @@ module MU
1022
1459
  sleep interval
1023
1460
  MU.log method_sym.to_s.bold+" "+e.inspect, MU::WARN, details: arguments
1024
1461
  retry
1025
- end
1462
+ end while !next_page_token.nil?
1026
1463
  end
1027
1464
  end
1028
1465