cloud-mu 2.1.0beta → 3.0.0beta

Sign up to get free protection for your applications and to get access to all the features.
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