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
@@ -18,38 +18,30 @@ module MU
18
18
  # A server pool as configured in {MU::Config::BasketofKittens::server_pools}
19
19
  class ServerPool < MU::Cloud::ServerPool
20
20
 
21
- @deploy = nil
22
- @project_id = nil
23
- @config = nil
24
- attr_reader :mu_name
25
- attr_reader :cloud_id
26
- attr_reader :config
27
-
28
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
29
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::server_pools}
30
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
31
- @deploy = mommacat
32
- @config = MU::Config.manxify(kitten_cfg)
33
- @cloud_id ||= cloud_id
34
- if !mu_name.nil?
35
- @mu_name = mu_name
36
- @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
37
- if !@project_id
38
- project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
39
- @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
40
- end
41
- elsif @config['scrub_mu_isms']
42
- @mu_name = @config['name']
43
- else
44
- @mu_name = @deploy.getResourceName(@config['name'])
45
- end
21
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
22
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
23
+ def initialize(**args)
24
+ super
25
+ @mu_name ||= @deploy.getResourceName(@config['name'])
46
26
  end
47
27
 
48
28
  # Called automatically by {MU::Deploy#createResources}
49
29
  def create
50
- @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
51
30
  port_objs = []
52
31
 
32
+ sa = MU::Config::Ref.get(@config['service_account'])
33
+ if !sa or !sa.kitten or !sa.kitten.cloud_desc
34
+ raise MuError, "Failed to get service account cloud id from #{@config['service_account'].to_s}"
35
+ end
36
+ @service_acct = MU::Cloud::Google.compute(:ServiceAccount).new(
37
+ email: sa.kitten.cloud_desc.email,
38
+ scopes: @config['scopes']
39
+ )
40
+ if !@config['scrub_mu_isms']
41
+ MU::Cloud::Google.grantDeploySecretAccess(@service_acct.email, credentials: @config['credentials'])
42
+ end
43
+
44
+
53
45
  @config['named_ports'].each { |port_cfg|
54
46
  port_objs << MU::Cloud::Google.compute(:NamedPort).new(
55
47
  name: port_cfg['name'],
@@ -77,20 +69,30 @@ module MU
77
69
  az = MU::Cloud::Google.listAZs(@config['region']).sample
78
70
  end
79
71
 
72
+ metadata = { # :items?
73
+ "startup-script" => @userdata
74
+ }
75
+ if @config['metadata']
76
+ desc[:metadata] = Hash[@config['metadata'].map { |m|
77
+ [m["key"], m["value"]]
78
+ }]
79
+ end
80
+ deploykey = @config['ssh_user']+":"+@deploy.ssh_public_key
81
+ if desc[:metadata]["ssh-keys"]
82
+ desc[:metadata]["ssh-keys"] += "\n"+deploykey
83
+ else
84
+ desc[:metadata]["ssh-keys"] = deploykey
85
+ end
86
+
80
87
  instance_props = MU::Cloud::Google.compute(:InstanceProperties).new(
81
88
  can_ip_forward: !@config['src_dst_check'],
82
89
  description: @deploy.deploy_id,
83
- # machine_type: "zones/"+az+"/machineTypes/"+size,
84
90
  machine_type: size,
91
+ service_accounts: [@service_acct],
85
92
  labels: labels,
86
93
  disks: MU::Cloud::Google::Server.diskConfig(@config, false, false, credentials: @config['credentials']),
87
94
  network_interfaces: MU::Cloud::Google::Server.interfaceConfig(@config, @vpc),
88
- metadata: {
89
- :items => [
90
- :key => "ssh-keys",
91
- :value => @config['ssh_user']+":"+@deploy.ssh_public_key
92
- ]
93
- },
95
+ metadata: metadata,
94
96
  tags: MU::Cloud::Google.compute(:Tags).new(items: [MU::Cloud::Google.nameStr(@mu_name)])
95
97
  )
96
98
 
@@ -132,9 +134,9 @@ module MU
132
134
  # TODO this thing supports based on CPU usage, LB usage, or an arbitrary Cloud
133
135
  # Monitoring metric. The default is "sustained 60%+ CPU usage". We should
134
136
  # support all that.
135
- # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyCpuUtilization
136
- # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyLoadBalancingUtilization
137
- # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeBeta/AutoscalingPolicyCustomMetricUtilization
137
+ # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyCpuUtilization
138
+ # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyLoadBalancingUtilization
139
+ # http://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ComputeV1/AutoscalingPolicyCustomMetricUtilization
138
140
  policy_obj = MU::Cloud::Google.compute(:AutoscalingPolicy).new(
139
141
  cooldown_period_sec: @config['default_cooldown'],
140
142
  max_num_replicas: @config['max_size'],
@@ -166,16 +168,156 @@ module MU
166
168
  end
167
169
 
168
170
  # Locate an existing ServerPool or ServerPools and return an array containing matching Google resource descriptors for those that match.
169
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
170
- # @param region [String]: The cloud provider region
171
- # @param tag_key [String]: A tag key to search.
172
- # @param tag_value [String]: The value of the tag specified by tag_key to match when searching by tag.
173
- # @param flags [Hash]: Optional flags
174
- # @return [Array<Hash<String,OpenStruct>>]: The cloud provider's complete descriptions of matching ServerPools
175
- def self.find(cloud_id: nil, region: MU.curRegion, tag_key: "Name", tag_value: nil, flags: {}, credentials: nil)
176
- flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
177
- MU.log "XXX ServerPool.find not yet implemented", MU::WARN
178
- return {}
171
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching ServerPools
172
+ def self.find(**args)
173
+ args[:project] ||= args[:habitat]
174
+ args[:project] ||= MU::Cloud::Google.defaultProject(args[:credentials])
175
+
176
+ regions = if args[:region]
177
+ [args[:region]]
178
+ else
179
+ MU::Cloud::Google.listRegions
180
+ end
181
+ found = {}
182
+
183
+ regions.each { |r|
184
+ begin
185
+ resp = MU::Cloud::Google.compute(credentials: args[:credentials]).list_region_instance_group_managers(args[:project], args[:region])
186
+ if resp and resp.items
187
+ resp.items.each { |igm|
188
+ found[igm.name] = igm
189
+ }
190
+ end
191
+ rescue ::Google::Apis::ClientError => e
192
+ raise e if !e.message.match(/forbidden: /)
193
+ end
194
+
195
+ begin
196
+ # XXX can these guys have name collisions? test this
197
+ MU::Cloud::Google.listAZs(r).each { |az|
198
+ resp = MU::Cloud::Google.compute(credentials: args[:credentials]).list_instance_group_managers(args[:project], az)
199
+ if resp and resp.items
200
+ resp.items.each { |igm|
201
+ found[igm.name] = igm
202
+ }
203
+ end
204
+ }
205
+ rescue ::Google::Apis::ClientError => e
206
+ raise e if !e.message.match(/forbidden: /)
207
+ end
208
+ }
209
+
210
+ return found
211
+ end
212
+
213
+ # Reverse-map our cloud description into a runnable config hash.
214
+ # We assume that any values we have in +@config+ are placeholders, and
215
+ # calculate our own accordingly based on what's live in the cloud.
216
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
217
+ bok = {
218
+ "cloud" => "Google",
219
+ "credentials" => @credentials,
220
+ "cloud_id" => @cloud_id,
221
+ "region" => @config['region'],
222
+ "project" => @project_id,
223
+ }
224
+ bok['name'] = cloud_desc.name
225
+
226
+ scalers = if cloud_desc.zone and cloud_desc.zone.match(/-[a-z]$/)
227
+ bok['availability_zone'] = cloud_desc.zone.sub(/.*?\/([^\/]+)$/, '\1')
228
+ MU::Cloud::Google.compute(credentials: @credentials).list_autoscalers(@project_id, bok['availability_zone'])
229
+ else
230
+ MU::Cloud::Google.compute(credentials: @credentials).list_region_autoscalers(@project_id, @config['region'], filter: "target eq #{cloud_desc.self_link}")
231
+ end
232
+
233
+ if scalers and scalers.items and scalers.items.size > 0
234
+ scaler = scalers.items.first
235
+ MU.log bok['name'], MU::WARN, details: scaler.autoscaling_policy
236
+ # scaler.cpu_utilization.utilization_target
237
+ # scaler.cool_down_period_sec
238
+ bok['min_size'] = scaler.autoscaling_policy.min_num_replicas
239
+ bok['max_size'] = scaler.autoscaling_policy.max_num_replicas
240
+ else
241
+ bok['min_size'] = bok['max_size'] = cloud_desc.target_size
242
+ end
243
+ if cloud_desc.auto_healing_policies and cloud_desc.auto_healing_policies.size > 0
244
+ MU.log bok['name'], MU::WARN, details: cloud_desc.auto_healing_policies
245
+ end
246
+
247
+ template = MU::Cloud::Google.compute(credentials: @credentials).get_instance_template(@project_id, cloud_desc.instance_template.sub(/.*?\/([^\/]+)$/, '\1'))
248
+
249
+ iface = template.properties.network_interfaces.first
250
+ iface.network.match(/(?:^|\/)projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/)
251
+ vpc_proj = Regexp.last_match[1]
252
+ vpc_id = Regexp.last_match[2]
253
+
254
+ bok['vpc'] = MU::Config::Ref.get(
255
+ id: vpc_id,
256
+ cloud: "Google",
257
+ habitat: MU::Config::Ref.get(
258
+ id: vpc_proj,
259
+ cloud: "Google",
260
+ credentials: @credentials,
261
+ type: "habitats"
262
+ ),
263
+ credentials: @credentials,
264
+ type: "vpcs",
265
+ subnet_pref: "any" # "anywhere in this VPC" is what matters
266
+ )
267
+
268
+ bok['basis'] = {
269
+ "launch_config" => {
270
+ "name" => bok['name']
271
+ }
272
+ }
273
+
274
+ template.properties.disks.each { |disk|
275
+ if disk.initialize_params.source_image and disk.boot
276
+ bok['basis']['launch_config']['image_id'] ||= disk.initialize_params.source_image.sub(/^https:\/\/www\.googleapis\.com\/compute\/[^\/]+\//, '')
277
+ elsif disk.type != "SCRATCH"
278
+ bok['basis']['launch_config']['storage'] ||= []
279
+ storage_blob = {
280
+ "size" => disk.initialize_params.disk_size_gb,
281
+ "device" => "/dev/xvd"+(disk.index+97).chr.downcase
282
+ }
283
+ bok['basis']['launch_config']['storage'] << storage_blob
284
+ else
285
+ MU.log "Need to sort out scratch disks", MU::WARN, details: disk
286
+ end
287
+
288
+ }
289
+
290
+ if template.properties.labels
291
+ bok['tags'] = template.properties.labels.keys.map { |k| { "key" => k, "value" => template.properties.labels[k] } }
292
+ end
293
+ if template.properties.tags and template.properties.tags.items and template.properties.tags.items.size > 0
294
+ bok['network_tags'] = template.properties.tags.items
295
+ end
296
+ bok['src_dst_check'] = !template.properties.can_ip_forward
297
+ bok['basis']['launch_config']['size'] = template.properties.machine_type.sub(/.*?\/([^\/]+)$/, '\1')
298
+ bok['project'] = @project_id
299
+ if template.properties.service_accounts
300
+ bok['scopes'] = template.properties.service_accounts.map { |sa| sa.scopes }.flatten.uniq
301
+ end
302
+ if template.properties.metadata and template.properties.metadata.items
303
+ bok['metadata'] = template.properties.metadata.items.map { |m| MU.structToHash(m) }
304
+ end
305
+
306
+ # Skip nodes that are just members of GKE clusters
307
+ if bok['name'].match(/^gke-.*?-[a-f0-9]+-[a-z0-9]+$/) and
308
+ bok['basis']['launch_config']['image_id'].match(/(:?^|\/)projects\/gke-node-images\//)
309
+ gke_ish = true
310
+ bok['network_tags'].each { |tag|
311
+ gke_ish = false if !tag.match(/^gke-/)
312
+ }
313
+ if gke_ish
314
+ MU.log "ServerPool #{bok['name']} appears to belong to a ContainerCluster, skipping adoption", MU::NOTICE
315
+ return nil
316
+ end
317
+ end
318
+ #MU.log bok['name'], MU::WARN, details: [cloud_desc, template]
319
+
320
+ bok
179
321
  end
180
322
 
181
323
  # Cloud-specific configuration properties.
@@ -184,6 +326,15 @@ module MU
184
326
  def self.schema(config)
185
327
  toplevel_required = []
186
328
  schema = {
329
+ "ssh_user" => MU::Cloud::Google::Server.schema(config)[1]["ssh_user"],
330
+ "metadata" => MU::Cloud::Google::Server.schema(config)[1]["metadata"],
331
+ "service_account" => MU::Cloud::Google::Server.schema(config)[1]["service_account"],
332
+ "scopes" => MU::Cloud::Google::Server.schema(config)[1]["scopes"],
333
+ "network_tags" => MU::Cloud::Google::Server.schema(config)[1]["network_tags"],
334
+ "availability_zone" => {
335
+ "type" => "string",
336
+ "description" => "Target a specific availability zone for this pool, which will create zonal instance managers and scalers instead of regional ones."
337
+ },
187
338
  "named_ports" => {
188
339
  "type" => "array",
189
340
  "items" => {
@@ -211,6 +362,38 @@ module MU
211
362
  # @return [Boolean]: True if validation succeeded, False otherwise
212
363
  def self.validateConfig(pool, configurator)
213
364
  ok = true
365
+ start = Time.now
366
+ pool['project'] ||= MU::Cloud::Google.defaultProject(pool['credentials'])
367
+ if pool['service_account']
368
+ pool['service_account']['cloud'] = "Google"
369
+ pool['service_account']['habitat'] ||= pool['project']
370
+ found = MU::Config::Ref.get(pool['service_account'])
371
+ if found.id and !found.kitten
372
+ MU.log "GKE pool #{pool['name']} failed to locate service account #{pool['service_account']} in project #{pool['project']}", MU::ERR
373
+ ok = false
374
+ end
375
+ else
376
+ user = {
377
+ "name" => pool['name'],
378
+ "cloud" => "Google",
379
+ "project" => pool["project"],
380
+ "credentials" => pool["credentials"],
381
+ "type" => "service"
382
+ }
383
+ configurator.insertKitten(user, "users", true)
384
+ pool['dependencies'] ||= []
385
+ pool['service_account'] = MU::Config::Ref.get(
386
+ type: "users",
387
+ cloud: "Google",
388
+ name: pool["name"],
389
+ project: pool["project"],
390
+ credentials: pool["credentials"]
391
+ )
392
+ pool['dependencies'] << {
393
+ "type" => "user",
394
+ "name" => pool["name"]
395
+ }
396
+ end
214
397
 
215
398
  pool['named_ports'] ||= []
216
399
  if !pool['named_ports'].include?({"name" => "ssh", "port" => 22})
@@ -224,8 +407,9 @@ module MU
224
407
  ok = false if launch['size'].nil?
225
408
 
226
409
  if launch['image_id'].nil?
227
- if MU::Config.google_images.has_key?(pool['platform'])
228
- launch['image_id'] = configurator.getTail("server_pool"+pool['name']+"Image", value: MU::Config.google_images[pool['platform']], prettyname: "server_pool"+pool['name']+"Image", cloudtype: "Google::Apis::ComputeBeta::Image")
410
+ img_id = MU::Cloud.getStockImage("Google", platform: pool['platform'])
411
+ if img_id
412
+ launch['image_id'] = configurator.getTail("server_pool"+pool['name']+"Image", value: img_id, prettyname: "server_pool"+pool['name']+"Image", cloudtype: "Google::Apis::ComputeV1::Image")
229
413
  else
230
414
  MU.log "No image specified for #{pool['name']} and no default available for platform #{pool['platform']}", MU::ERR, details: launch
231
415
  ok = false
@@ -270,6 +454,7 @@ module MU
270
454
  # @return [void]
271
455
  def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
272
456
  flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
457
+ return if !MU::Cloud::Google::Habitat.isLive?(flags["project"], credentials)
273
458
 
274
459
  if !flags["global"]
275
460
  ["region_autoscaler", "region_instance_group_manager"].each { |type|
@@ -17,45 +17,160 @@ module MU
17
17
  class Google
18
18
  # A user as configured in {MU::Config::BasketofKittens::users}
19
19
  class User < MU::Cloud::User
20
- @deploy = nil
21
- @config = nil
22
- attr_reader :mu_name
23
- attr_reader :config
24
- attr_reader :cloud_id
25
-
26
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
27
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::users}
28
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
29
- @deploy = mommacat
30
- @config = MU::Config.manxify(kitten_cfg)
31
- @cloud_id ||= cloud_id
32
- @mu_name ||= @deploy.getResourceName(@config["name"])
20
+
21
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
22
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
23
+ def initialize(**args)
24
+ super
25
+
26
+ # If we're being reverse-engineered from a cloud descriptor, use that
27
+ # to determine what sort of account we are.
28
+ if args[:from_cloud_desc]
29
+ MU::Cloud::Google.admin_directory
30
+ MU::Cloud::Google.iam
31
+ if args[:from_cloud_desc].class == ::Google::Apis::AdminDirectoryV1::User
32
+ @config['type'] = "interactive"
33
+ elsif args[:from_cloud_desc].class == ::Google::Apis::IamV1::ServiceAccount
34
+ @config['type'] = "service"
35
+ @config['name'] = args[:from_cloud_desc].display_name
36
+ if @config['name'].nil? or @config['name'].empty?
37
+ @config['name'] = args[:from_cloud_desc].name.sub(/.*?\/([^\/@]+)(?:@[^\/]*)?$/, '\1')
38
+ end
39
+ @cloud_id = args[:from_cloud_desc].name
40
+ else
41
+ raise MuError, "Google::User got from_cloud_desc arg of class #{args[:from_cloud_desc].class.name}, but doesn't know what to do with it"
42
+ end
43
+ end
44
+
45
+ @mu_name ||= if (@config['unique_name'] or @config['type'] == "service") and !@config['scrub_mu_isms']
46
+ @deploy.getResourceName(@config["name"])
47
+ else
48
+ @config['name']
49
+ end
50
+
33
51
  end
34
52
 
35
53
  # Called automatically by {MU::Deploy#createResources}
36
54
  def create
37
- if @config['type'] == "interactive"
38
- bind_human_user
39
- else
55
+ if @config['type'] == "service"
56
+ acct_id = @config['scrub_mu_isms'] ? @config['name'] : @deploy.getResourceName(@config["name"], max_length: 30).downcase
40
57
  req_obj = MU::Cloud::Google.iam(:CreateServiceAccountRequest).new(
41
- account_id: @deploy.getResourceName(@config["name"], max_length: 30).downcase,
58
+ account_id: acct_id,
42
59
  service_account: MU::Cloud::Google.iam(:ServiceAccount).new(
43
- display_name: @mu_name
60
+ display_name: @mu_name,
61
+ description: @config['scrub_mu_isms'] ? nil : @deploy.deploy_id
62
+ )
63
+ )
64
+ if @config['use_if_exists']
65
+ # XXX maybe just set @cloud_id to projects/#{@project_id}/serviceAccounts/#{@mu_name}@#{@project_id}.iam.gserviceaccount.com and see if cloud_desc returns something
66
+ found = MU::Cloud::Google::User.find(project: @project_id, cloud_id: @mu_name)
67
+ if found.size == 1
68
+ @cloud_id = found.keys.first
69
+ MU.log "Service account #{@cloud_id} already existed, using it"
70
+ end
71
+ end
72
+
73
+ if !@cloud_id
74
+ MU.log "Creating service account #{@mu_name}"
75
+ resp = MU::Cloud::Google.iam(credentials: @config['credentials']).create_service_account(
76
+ "projects/"+@config['project'],
77
+ req_obj
44
78
  )
79
+ @cloud_id = resp.name
80
+ end
81
+
82
+ # make sure we've been created before moving on
83
+ begin
84
+ cloud_desc
85
+ rescue ::Google::Apis::ClientError => e
86
+ if e.message.match(/notFound:/)
87
+ sleep 3
88
+ retry
89
+ end
90
+ end
91
+ elsif @config['external']
92
+ @cloud_id = @config['email']
93
+ MU::Cloud::Google::Role.bindFromConfig("user", @cloud_id, @config['roles'], credentials: @config['credentials'])
94
+ else
95
+ if !@config['email']
96
+ domains = MU::Cloud::Google.admin_directory(credentials: @credentials).list_domains(@customer)
97
+ @config['email'] = @mu_name.gsub(/@.*/, "")+"@"+domains.domains.first.domain_name
98
+ end
99
+
100
+ username_obj = MU::Cloud::Google.admin_directory(:UserName).new(
101
+ given_name: (@config['given_name'] || @config['name']),
102
+ family_name: (@config['family_name'] || @deploy.deploy_id),
103
+ full_name: @mu_name
45
104
  )
46
- MU.log "Creating service account #{@mu_name}"
47
- MU::Cloud::Google.iam(credentials: @config['credentials']).create_service_account(
48
- "projects/"+@config['project'],
49
- req_obj
105
+
106
+ user_obj = MU::Cloud::Google.admin_directory(:User).new(
107
+ name: username_obj,
108
+ primary_email: @config['email'],
109
+ suspended: @config['suspend'],
110
+ is_admin: @config['admin'],
111
+ password: MU.generateWindowsPassword,
112
+ change_password_at_next_login: (@config.has_key?('force_password_change') ? @config['force_password_change'] : true)
50
113
  )
114
+
115
+ MU.log "Creating user #{@mu_name}", details: user_obj
116
+ resp = MU::Cloud::Google.admin_directory(credentials: @credentials).insert_user(user_obj)
117
+ @cloud_id = resp.primary_email
118
+
51
119
  end
52
120
  end
53
121
 
54
122
  # Called automatically by {MU::Deploy#createResources}
55
123
  def groom
56
- if @config['type'] == "interactive"
57
- bind_human_user
124
+ if @config['external']
125
+ MU::Cloud::Google::Role.bindFromConfig("user", @cloud_id, @config['roles'], credentials: @config['credentials'])
126
+ elsif @config['type'] == "interactive"
127
+ need_update = false
128
+ MU::Cloud::Google::Role.bindFromConfig("user", @cloud_id, @config['roles'], credentials: @config['credentials'])
129
+
130
+ if @config['force_password_change'] and !cloud_desc.change_password_at_next_login
131
+ MU.log "Forcing #{@mu_name} to change their password at next login", MU::NOTICE
132
+ need_update = true
133
+ elsif @config.has_key?("force_password_change") and
134
+ !@config['force_password_change'] and
135
+ cloud_desc.change_password_at_next_login
136
+ MU.log "No longer forcing #{@mu_name} to change their password at next login", MU::NOTICE
137
+ need_update = true
138
+ end
139
+ if @config['admin'] != cloud_desc.is_admin
140
+ MU.log "Setting 'is_admin' flag to #{@config['admin'].to_s} for directory user #{@mu_name}", MU::NOTICE
141
+ MU::Cloud::Google.admin_directory(credentials: @credentials).make_user_admin(@cloud_id, MU::Cloud::Google.admin_directory(:UserMakeAdmin).new(status: @config['admin']))
142
+ end
143
+
144
+ if @config['suspend'] != cloud_desc.suspended
145
+ need_update = true
146
+ end
147
+ if cloud_desc.name.given_name != (@config['given_name'] || @config['name']) or
148
+ cloud_desc.name.family_name != (@config['family_name'] || @deploy.deploy_id) or
149
+ cloud_desc.primary_email != @config['email']
150
+ need_update = true
151
+ end
152
+
153
+ if need_update
154
+ username_obj = MU::Cloud::Google.admin_directory(:UserName).new(
155
+ given_name: (@config['given_name'] || @config['name']),
156
+ family_name: (@config['family_name'] || @deploy.deploy_id),
157
+ full_name: @mu_name
158
+ )
159
+ user_obj = MU::Cloud::Google.admin_directory(:User).new(
160
+ name: username_obj,
161
+ primary_email: @config['email'],
162
+ suspended: @config['suspend'],
163
+ change_password_at_next_login: (@config.has_key?('force_password_change') ? @config['force_password_change'] : true)
164
+ )
165
+
166
+ MU.log "Updating directory user #{@mu_name}", MU::NOTICE, details: user_obj
167
+
168
+ resp = MU::Cloud::Google.admin_directory(credentials: @credentials).update_user(@cloud_id, user_obj)
169
+ @cloud_id = resp.primary_email
170
+ end
171
+
58
172
  else
173
+ MU::Cloud::Google::Role.bindFromConfig("serviceAccount", @cloud_id.gsub(/.*?\/([^\/]+)$/, '\1'), @config['roles'], credentials: @config['credentials'])
59
174
  if @config['create_api_key']
60
175
  resp = MU::Cloud::Google.iam(credentials: @config['credentials']).list_project_service_account_keys(
61
176
  cloud_desc.name
@@ -73,34 +188,32 @@ module MU
73
188
  end
74
189
 
75
190
  # Retrieve the cloud descriptor for this resource.
191
+ # @return [Google::Apis::Core::Hashable]
76
192
  def cloud_desc
77
- if @config['type'] == "interactive"
78
- return nil
79
- else
80
- resp = MU::Cloud::Google.iam(credentials: @config['credentials']).list_project_service_accounts(
81
- "projects/"+@config["project"]
82
- )
83
-
84
- if resp and resp.accounts
85
- resp.accounts.each { |sa|
86
- if sa.display_name and sa.display_name == @mu_name
87
- return sa
88
- end
89
- }
193
+ if @config['type'] == "interactive" or !@config['type']
194
+ @config['type'] ||= "interactive"
195
+ if !@config['external']
196
+ return MU::Cloud::Google.admin_directory(credentials: @config['credentials']).get_user(@cloud_id)
197
+ else
198
+ return nil
90
199
  end
200
+ else
201
+ @config['type'] ||= "service"
202
+ return MU::Cloud::Google.iam(credentials: @config['credentials']).get_project_service_account(@cloud_id)
91
203
  end
204
+
92
205
  end
93
206
 
94
207
  # Return the metadata for this user configuration
95
208
  # @return [Hash]
96
209
  def notify
97
- description = MU.structToHash(cloud_desc)
98
- if description
99
- description.delete(:etag)
100
- return description
210
+ description = if !@config['external']
211
+ MU.structToHash(cloud_desc)
212
+ else
213
+ {}
101
214
  end
102
- {
103
- }
215
+ description.delete(:etag)
216
+ description
104
217
  end
105
218
 
106
219
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -113,7 +226,7 @@ module MU
113
226
  # Denote whether this resource implementation is experiment, ready for
114
227
  # testing, or ready for production use.
115
228
  def self.quality
116
- MU::Cloud::ALPHA
229
+ MU::Cloud::RELEASE
117
230
  end
118
231
 
119
232
  # Remove all users associated with the currently loaded deployment.
@@ -122,6 +235,33 @@ module MU
122
235
  # @param region [String]: The cloud provider region
123
236
  # @return [void]
124
237
  def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
238
+ my_domains = MU::Cloud::Google.getDomains(credentials)
239
+ my_org = MU::Cloud::Google.getOrg(credentials)
240
+
241
+ # We don't have a good way of tagging directory users, so we rely
242
+ # on the known parameter, which is pulled from deployment metadata
243
+ if flags['known'] and my_org
244
+ dir_users = MU::Cloud::Google.admin_directory(credentials: credentials).list_users(customer: MU::Cloud::Google.customerID(credentials)).users
245
+ if dir_users
246
+ dir_users.each { |user|
247
+ if flags['known'].include?(user.primary_email)
248
+ MU.log "Deleting user #{user.primary_email} from #{my_org.display_name}", details: user
249
+ if !noop
250
+ MU::Cloud::Google.admin_directory(credentials: credentials).delete_user(user.id)
251
+ end
252
+ end
253
+ }
254
+
255
+ flags['known'].each { |user_email|
256
+ next if user_email.nil?
257
+ next if !user_email.match(/^[^\/]+@[^\/]+$/)
258
+
259
+ MU::Cloud::Google::Role.removeBindings("user", user_email, credentials: credentials, noop: noop)
260
+ }
261
+
262
+ end
263
+ end
264
+
125
265
  flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
126
266
  resp = MU::Cloud::Google.iam(credentials: credentials).list_project_service_accounts(
127
267
  "projects/"+flags["project"]
@@ -129,7 +269,8 @@ module MU
129
269
 
130
270
  if resp and resp.accounts and MU.deploy_id
131
271
  resp.accounts.each { |sa|
132
- if sa.display_name and sa.display_name.match(/^#{Regexp.quote(MU.deploy_id)}-/i)
272
+ if (sa.description and sa.description == MU.deploy_id) or
273
+ (sa.display_name and sa.display_name.match(/^#{Regexp.quote(MU.deploy_id)}-/i))
133
274
  begin
134
275
  MU.log "Deleting service account #{sa.name}", details: sa
135
276
  if !noop
@@ -143,30 +284,167 @@ module MU
143
284
  end
144
285
  end
145
286
 
146
- # Locate an existing user.
147
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
148
- # @param region [String]: The cloud provider region.
149
- # @param flags [Hash]: Optional flags
150
- # @return [OpenStruct]: The cloud provider's complete descriptions of matching user group.
151
- def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
152
- flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
153
- found = nil
154
- resp = MU::Cloud::Google.iam(credentials: credentials).list_project_service_accounts(
155
- "projects/"+flags["project"]
156
- )
287
+ # Locate and return cloud provider descriptors of this resource type
288
+ # which match the provided parameters, or all visible resources if no
289
+ # filters are specified. At minimum, implementations of +find+ must
290
+ # honor +credentials+ and +cloud_id+ arguments. We may optionally
291
+ # support other search methods, such as +tag_key+ and +tag_value+, or
292
+ # cloud-specific arguments like +project+. See also {MU::MommaCat.findStray}.
293
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
294
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources
295
+ def self.find(**args)
296
+ cred_cfg = MU::Cloud::Google.credConfig(args[:credentials])
297
+ args[:project] ||= args[:habitat]
157
298
 
158
- if resp and resp.accounts
159
- resp.accounts.each { |sa|
160
- if sa.display_name and sa.display_name == cloud_id
161
- found ||= {}
162
- found[cloud_id] = sa
299
+ found = {}
300
+
301
+ if args[:cloud_id] and args[:flags] and
302
+ args[:flags]["skip_provider_owned"] and
303
+ MU::Cloud::Google::User.cannedServiceAcctName?(args[:cloud_id])
304
+ return found
305
+ end
306
+
307
+ # If the project id is embedded in the cloud_id, honor it
308
+ if args[:cloud_id]
309
+ if args[:cloud_id].match(/projects\/(.+?)\//)
310
+ args[:project] = Regexp.last_match[1]
311
+ elsif args[:cloud_id].match(/@([^\.]+)\.iam\.gserviceaccount\.com$/)
312
+ args[:project] = Regexp.last_match[1]
313
+ end
314
+ end
315
+
316
+ if args[:project]
317
+ # project-local service accounts
318
+ resp = begin
319
+ MU::Cloud::Google.iam(credentials: args[:credentials]).list_project_service_accounts(
320
+ "projects/"+args[:project]
321
+ )
322
+ rescue ::Google::Apis::ClientError => e
323
+ MU.log "Do not have permissions to retrieve service accounts for project #{args[:project]}", MU::WARN
324
+ end
325
+
326
+ if resp and resp.accounts
327
+ resp.accounts.each { |sa|
328
+ if args[:flags] and args[:flags]["skip_provider_owned"] and
329
+ MU::Cloud::Google::User.cannedServiceAcctName?(sa.name)
330
+ next
331
+ end
332
+ if !args[:cloud_id] or (sa.display_name and sa.display_name == args[:cloud_id]) or (sa.name and sa.name == args[:cloud_id]) or (sa.email and sa.email == args[:cloud_id])
333
+ found[sa.name] = sa
334
+ end
335
+ }
336
+ end
337
+ else
338
+ if cred_cfg['masquerade_as']
339
+ resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).list_users(customer: MU::Cloud::Google.customerID(args[:credentials]), show_deleted: false)
340
+ if resp and resp.users
341
+ resp.users.each { |u|
342
+ found[u.primary_email] = u
343
+ }
163
344
  end
164
- }
345
+ end
165
346
  end
166
347
 
167
348
  found
168
349
  end
169
350
 
351
+ # Try to determine whether the given string looks like a pre-configured
352
+ # GCP service account, as distinct from one we might create or manage
353
+ def self.cannedServiceAcctName?(name)
354
+ return false if !name
355
+ name.match(/\b\d+\-compute@developer\.gserviceaccount\.com$/) or
356
+ name.match(/\bproject-\d+@storage-transfer-service\.iam\.gserviceaccount\.com$/) or
357
+ name.match(/\b\d+@cloudbuild\.gserviceaccount\.com$/) or
358
+ name.match(/\bservice-\d+@containerregistry\.iam\.gserviceaccount\.com$/) or
359
+ name.match(/\bservice-\d+@container-analysis\.iam\.gserviceaccount\.com$/) or
360
+ name.match(/\bservice-\d+@gcp-sa-bigquerydatatransfer\.iam\.gserviceaccount\.com$/) or
361
+ name.match(/\bservice-\d+@gcp-sa-cloudasset\.iam\.gserviceaccount\.com$/) or
362
+ name.match(/\bservice-\d+@gcp-sa-cloudiot\.iam\.gserviceaccount\.com$/) or
363
+ name.match(/\bservice-\d+@gcp-sa-cloudscheduler\.iam\.gserviceaccount\.com$/) or
364
+ name.match(/\bservice-\d+@compute-system\.iam\.gserviceaccount\.com$/) or
365
+ name.match(/\bservice-\d+@container-engine-robot\.iam\.gserviceaccount\.com$/) or
366
+ name.match(/\bservice-\d+@gcp-admin-robot\.iam\.gserviceaccount\.com$/) or
367
+ name.match(/\bservice-\d+@gcp-sa-containerscanning\.iam\.gserviceaccount\.com$/) or
368
+ name.match(/\bservice-\d+@dataflow-service-producer-prod\.iam\.gserviceaccount\.com$/) or
369
+ name.match(/\bservice-\d+@dataproc-accounts\.iam\.gserviceaccount\.com$/) or
370
+ name.match(/\bservice-\d+@endpoints-portal\.iam\.gserviceaccount\.com$/) or
371
+ name.match(/\bservice-\d+@cloud-filer\.iam\.gserviceaccount\.com$/) or
372
+ name.match(/\bservice-\d+@cloud-redis\.iam\.gserviceaccount\.com$/) or
373
+ name.match(/\bservice-\d+@firebase-rules\.iam\.gserviceaccount\.com$/) or
374
+ name.match(/\bservice-\d+@cloud-tpu\.iam\.gserviceaccount\.com$/) or
375
+ name.match(/\bservice-\d+@gcp-sa-vpcaccess\.iam\.gserviceaccount\.com$/) or
376
+ name.match(/\bservice-\d+@gcp-sa-websecurityscanner\.iam\.gserviceaccount\.com$/) or
377
+ name.match(/\bservice-\d+@sourcerepo-service-accounts\.iam\.gserviceaccount\.com$/) or
378
+ name.match(/\bp\d+\-\d+@gcp-sa-logging\.iam\.gserviceaccount\.com$/)
379
+ end
380
+
381
+ # We can either refer to a service account, which is scoped to a project
382
+ # (a +Habitat+ in Mu parlance), or a "real" user, which comes from
383
+ # an external directory like GMail, GSuite, or Cloud Identity.
384
+ def self.canLiveIn
385
+ [:Habitat, nil]
386
+ end
387
+
388
+ # Reverse-map our cloud description into a runnable config hash.
389
+ # We assume that any values we have in +@config+ are placeholders, and
390
+ # calculate our own accordingly based on what's live in the cloud.
391
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
392
+ if MU::Cloud::Google::User.cannedServiceAcctName?(@cloud_id)
393
+ return nil
394
+ end
395
+
396
+ bok = {
397
+ "cloud" => "Google",
398
+ "credentials" => @config['credentials']
399
+ }
400
+
401
+ if cloud_desc.nil?
402
+ MU.log @config['name']+" couldn't fetch its cloud descriptor", MU::WARN, details: @cloud_id
403
+ return nil
404
+ end
405
+
406
+ user_roles = MU::Cloud::Google::Role.getAllBindings(@config['credentials'])["by_entity"]
407
+
408
+ if cloud_desc.nil?
409
+ MU.log "FAILED TO FIND CLOUD DESCRIPTOR FOR #{self}", MU::ERR, details: @config
410
+ return nil
411
+ end
412
+
413
+ bok['name'] = @config['name']
414
+ bok['cloud_id'] = @cloud_id
415
+ bok['type'] = @config['type']
416
+ bok['type'] ||= "service"
417
+ if bok['type'] == "service"
418
+ bok['project'] = @project_id
419
+ keys = MU::Cloud::Google.iam(credentials: @config['credentials']).list_project_service_account_keys(@cloud_id)
420
+
421
+ if keys and keys.keys and keys.keys.size > 0
422
+ bok['create_api_key'] = true
423
+ end
424
+ # MU.log "service account #{@cloud_id}", MU::NOTICE, details: MU::Cloud::Google.iam(credentials: @config['credentials']).get_project_service_account_iam_policy(cloud_desc.name)
425
+ if user_roles["serviceAccount"] and
426
+ user_roles["serviceAccount"][bok['cloud_id']] and
427
+ user_roles["serviceAccount"][bok['cloud_id']].size > 0
428
+ bok['roles'] = MU::Cloud::Google::Role.entityBindingsToSchema(user_roles["serviceAccount"][bok['cloud_id']])
429
+ end
430
+ else
431
+ if user_roles["user"] and
432
+ user_roles["user"][bok['cloud_id']] and
433
+ user_roles["user"][bok['cloud_id']].size > 0
434
+ bok['roles'] = MU::Cloud::Google::Role.entityBindingsToSchema(user_roles["user"][bok['cloud_id']], credentials: @config['credentials'])
435
+ end
436
+ bok['given_name'] = cloud_desc.name.given_name
437
+ bok['family_name'] = cloud_desc.name.family_name
438
+ bok['email'] = cloud_desc.primary_email
439
+ bok['suspend'] = cloud_desc.suspended
440
+ bok['admin'] = cloud_desc.is_admin
441
+ end
442
+
443
+ bok['use_if_exists'] = true
444
+
445
+ bok
446
+ end
447
+
170
448
  # Cloud-specific configuration properties.
171
449
  # @param config [MU::Config]: The calling MU::Config object
172
450
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -175,24 +453,68 @@ module MU
175
453
  schema = {
176
454
  "name" => {
177
455
  "type" => "string",
178
- "description" => "This must be the email address of an existing Google user account (+foo@gmail.com+), or of a federated GSuite or Cloud Identity domain account from your organization."
456
+ "description" => "If the +type+ of this account is not +service+, this can include an optional @domain component (<tt>foo@example.com</tt>), which is equivalent to the +domain+ configuration option. The following rules apply to +directory+ (non-<tt>service</tt>) accounts only:
457
+
458
+ If the domain portion is not specified, and we manage exactly one GSuite or Cloud Identity domain, we will attempt to create the user in that domain.
459
+
460
+ If we do not manage any domains, and none are specified, we will assume <tt>@gmail.com</tt> for the domain and attempt to bind an existing external GMail user to roles under our jurisdiction.
461
+
462
+ If the domain portion is specified, and our credentials can manage that domain via GSuite or Cloud Identity, we will attempt to create the user in that domain.
463
+
464
+ If it is a domain we do not manage, we will attempt to bind an existing external user from that domain to roles under our jurisdiction.
465
+
466
+ If we are binding (rather than creating) a user and no roles are specified, we will default to +roles/viewer+ at the organization scope. If our credentials do not manage an organization, we will grant this role in our default project.
467
+
468
+ "
469
+ },
470
+ "domain" => {
471
+ "type" => "string",
472
+ "description" => "If creating or binding an +interactive+ user, this is the domain of which the user should be a member. This can instead be embedded in the {name} field: +foo@example.com+."
473
+ },
474
+ "given_name" => {
475
+ "type" => "string",
476
+ "description" => "Optionally set the +given_name+ field of a +directory+ account. Ignored for +service+ accounts."
477
+ },
478
+ "first_name" => {
479
+ "type" => "string",
480
+ "description" => "Alias for +given_name+"
481
+ },
482
+ "family_name" => {
483
+ "type" => "string",
484
+ "description" => "Optionally set the +family_name+ field of a +directory+ account. Ignored for +service+ accounts."
485
+ },
486
+ "last_name" => {
487
+ "type" => "string",
488
+ "description" => "Alias for +family_name+"
489
+ },
490
+ "email" => {
491
+ "type" => "string",
492
+ "description" => "Canonical email address for a +directory+ user. If not specified, will be set to +name@domain+."
493
+ },
494
+ "external" => {
495
+ "type" => "boolean",
496
+ "description" => "Explicitly flag this user as originating from an external domain. This should always autodetect correctly."
497
+ },
498
+ "admin" => {
499
+ "type" => "boolean",
500
+ "description" => "If the user is +interactive+ and resides in a domain we manage, set their +is_admin+ flag.",
501
+ "default" => false
502
+ },
503
+ "suspend" => {
504
+ "type" => "boolean",
505
+ "description" => "If the user is +interactive+ and resides in a domain we manage, this can be used to lock their account.",
506
+ "default" => false
179
507
  },
180
508
  "type" => {
181
509
  "type" => "string",
182
- "description" => "'interactive' will attempt to bind an existing user; 'service' will create a service account and generate API keys"
510
+ "description" => "'interactive' will either attempt to bind an existing user to a role under our jurisdiction, or create a new directory user, depending on the domain of the user specified and whether we manage any directories; 'service' will create a service account and generate API keys.",
511
+ "enum" => ["interactive", "service"],
512
+ "default" => "interactive"
183
513
  },
184
514
  "roles" => {
185
515
  "type" => "array",
186
516
  "description" => "One or more Google IAM roles to associate with this user.",
187
- "default" => ["roles/viewer"],
188
- "items" => {
189
- "type" => "string",
190
- "description" => "One or more Google IAM roles to associate with this user. Google Cloud human user accounts (as distinct from service accounts) are not created directly; pre-existing Google accounts are associated with a project by being bound to one or more roles in that project. If no roles are specified, we default to +roles/viewer+, which permits read-only access project-wide."
191
- }
192
- },
193
- "project" => {
194
- "type" => "string",
195
- "description" => "The project into which to deploy resources"
517
+ "items" => MU::Cloud::Google::Role.ref_schema
196
518
  }
197
519
  }
198
520
  [toplevel_required, schema]
@@ -205,75 +527,107 @@ module MU
205
527
  def self.validateConfig(user, configurator)
206
528
  ok = true
207
529
 
208
- # admin_directory only works in a GSuite environment
209
- if !user['name'].match(/@/i) and MU::Cloud::Google.credConfig(user['credentials'])['masquerade_as']
210
- # XXX flesh this check out, need to test with a GSuite site
211
- pp MU::Cloud::Google.admin_directory(credentials: user['credentials']).get_user(user['name'])
530
+ my_domains = MU::Cloud::Google.getDomains(user['credentials'])
531
+ my_org = MU::Cloud::Google.getOrg(user['credentials'])
532
+
533
+ # Deal with these name alias fields, here for the convenience of your
534
+ # easily confused english-centric type of person
535
+ user['given_name'] ||= user['first_name']
536
+ user['family_name'] ||= user['last_name']
537
+ user.delete("first_name")
538
+ user.delete("last_name")
539
+
540
+ if user['name'].match(/@(.*+)$/)
541
+ domain = Regexp.last_match[1].downcase
542
+ if domain and user['domain'] and domain != user['domain'].downcase
543
+ MU.log "User #{user['name']} had a domain component, but the domain field was also specified (#{user['domain']}) and they don't match."
544
+ ok = false
545
+ end
546
+ user['domain'] = domain
547
+ if user['type'] == "service"
548
+ MU.log "Username #{user['name']} appears to be a directory or external username, cannot use with 'service'", MU::ERR
549
+ ok = false
550
+ else
551
+ user['type'] = "interactive"
552
+ if !my_domains or !my_domains.include?(domain)
553
+ user['external'] = true
554
+
555
+ if !["gmail.com", "google.com"].include?(domain)
556
+ MU.log "#{user['name']} appears to be a member of a domain that our credentials (#{user['credentials']}) do not manage; attempts to grant access for this user may fail!", MU::WARN
557
+ end
558
+
559
+ if !user['roles'] or user['roles'].empty?
560
+ user['roles'] = [
561
+ {
562
+ "role" => {
563
+ "id" => "roles/viewer"
564
+ }
565
+ }
566
+ ]
567
+ MU.log "External Google user specified with no role binding, will grant 'viewer' in #{my_org ? "organization #{my_org.display_name}" : "project #{user['project']}"}", MU::WARN
568
+ end
569
+ else # this is actually targeting a domain we manage! yay!
570
+ end
571
+ end
572
+ elsif user['type'] != "service"
573
+ if !user['domain']
574
+ if my_domains.size == 1
575
+ user['domain'] = my_domains.first
576
+ elsif my_domains.size > 1
577
+ MU.log "Google interactive User #{user['name']} did not specify a domain, and we have multiple defaults available. Must specify exactly one.", MU::ERR, details: my_domains
578
+ ok = false
579
+ else
580
+ user['domain'] = "gmail.com"
581
+ end
582
+ end
212
583
  end
213
584
 
214
- if user['groups'] and user['groups'].size > 0 and
215
- !MU::Cloud::Google.credConfig(user['credentials'])['masquerade_as']
216
- MU.log "Cannot change Google group memberships in non-GSuite environments.\nVisit https://groups.google.com to manage groups.", MU::ERR
585
+ if user['domain']
586
+ user['email'] ||= user['name'].gsub(/@.*/, "")+"@"+user['domain']
587
+ end
588
+
589
+ if user['groups'] and user['groups'].size > 0 and my_org.nil?
590
+ MU.log "Cannot change Google group memberships with credentials that do not manage GSuite or Cloud Identity.\nVisit https://groups.google.com to manage groups.", MU::ERR
217
591
  ok = false
218
592
  end
219
593
 
594
+ if user['type'] == "service"
595
+ user['project'] ||= MU::Cloud::Google.defaultProject(user['credentials'])
596
+ end
597
+
220
598
  if user['type'] != "service" and user["create_api_key"]
221
599
  MU.log "Only service accounts can have API keys in Google Cloud", MU::ERR
222
600
  ok = false
223
601
  end
224
602
 
225
- ok
226
- end
227
-
228
- private
229
-
230
- def bind_human_user
231
- bindings = []
232
- ext_policy = MU::Cloud::Google.resource_manager(credentials: @config['credentials']).get_project_iam_policy(
233
- @config['project']
234
- )
603
+ user['dependencies'] ||= []
604
+ if user['roles']
605
+ user['roles'].each { |r|
606
+ if r['role'] and r['role']['name'] and
607
+ (!r['role']['deploy_id'] and !r['role']['id'])
608
+ user['dependencies'] << {
609
+ "type" => "role",
610
+ "name" => r['role']['name']
611
+ }
612
+ end
235
613
 
236
- change_needed = false
237
- @config['roles'].each { |role|
238
- seen = false
239
- ext_policy.bindings.each { |b|
240
- if b.role == role
241
- seen = true
242
- if !b.members.include?("user:"+@config['name'])
243
- change_needed = true
244
- b.members << "user:"+@config['name']
614
+ if !r["projects"] and !r["organizations"] and !r["folders"]
615
+ if my_org
616
+ r["organizations"] = [my_org.name]
617
+ else
618
+ r["projects"] = [
619
+ "id" => user["project"]
620
+ ]
245
621
  end
246
622
  end
247
623
  }
248
- if !seen
249
- ext_policy.bindings << MU::Cloud::Google.resource_manager(:Binding).new(
250
- role: role,
251
- members: ["user:"+@config['name']]
252
- )
253
- change_needed = true
254
- end
255
- }
256
-
257
- if change_needed
258
- req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
259
- policy: ext_policy
260
- )
261
- MU.log "Adding #{@config['name']} to Google Cloud project #{@config['project']}", details: @config['roles']
262
-
263
- begin
264
- MU::Cloud::Google.resource_manager(credentials: @config['credentials']).set_project_iam_policy(
265
- @config['project'],
266
- req_obj
267
- )
268
- rescue ::Google::Apis::ClientError => e
269
- if e.message.match(/does not exist/i) and !MU::Cloud::Google.credConfig(@config['credentials'])['masquerade_as']
270
- raise MuError, "User #{@config['name']} does not exist, and we cannot create Google user in non-GSuite environments.\nVisit https://accounts.google.com to create new accounts."
271
- end
272
- raise e
273
- end
274
624
  end
625
+
626
+ ok
275
627
  end
276
628
 
629
+ private
630
+
277
631
  end
278
632
  end
279
633
  end