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
@@ -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