cloud-mu 2.1.0beta → 3.0.0beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -28,6 +28,9 @@ module MU
28
28
  # Exception class for BoK parse or validation errors
29
29
  class ValidationError < MU::MuError
30
30
  end
31
+ # Exception class for duplicate resource names
32
+ class DuplicateNameError < MU::MuError
33
+ end
31
34
  # Exception class for deploy parameter (mu-deploy -p foo=bar) errors
32
35
  class DeployParamError < MuError
33
36
  end
@@ -41,8 +44,6 @@ module MU
41
44
  if $MU_CFG[cloud.downcase] and !$MU_CFG[cloud.downcase].empty?
42
45
  configured[cloud] = $MU_CFG[cloud.downcase].size
43
46
  configured[cloud] += 0.5 if cloudclass.hosted? # tiebreaker
44
- elsif cloudclass.hosted?
45
- configured[cloud] = 1
46
47
  end
47
48
  }
48
49
  if configured.size > 0
@@ -50,61 +51,22 @@ module MU
50
51
  configured[b] <=> configured[a]
51
52
  }.first
52
53
  else
54
+ MU::Cloud.supportedClouds.each { |cloud|
55
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
56
+ return cloud if cloudclass.hosted?
57
+ }
53
58
  return MU::Cloud.supportedClouds.first
54
59
  end
55
60
  end
56
61
 
57
62
  # The default grooming agent for new resources. Must exist in MU.supportedGroomers.
58
63
  def self.defaultGroomer
59
- "Chef"
64
+ MU.localOnly ? "Ansible" : "Chef"
60
65
  end
61
66
 
62
67
  attr_accessor :nat_routes
63
68
  attr_reader :skipinitialupdates
64
69
 
65
- attr_reader :google_images
66
- @@google_images = YAML.load(File.read("#{MU.myRoot}/modules/mu/defaults/google_images.yaml"))
67
- if File.exists?("#{MU.etcDir}/google_images.yaml")
68
- custom = YAML.load(File.read("#{MU.etcDir}/google_images.yaml"))
69
- @@google_images.merge!(custom) { |key, oldval, newval|
70
- if !oldval.is_a?(Hash) and !newval.nil?
71
- if !newval.nil?
72
- newval
73
- else
74
- oldval
75
- end
76
- else
77
- oldval.merge(newval)
78
- end
79
- }
80
- end
81
- # The list of known Google Images which we can use for a given platform
82
- def self.google_images
83
- @@google_images
84
- end
85
-
86
- attr_reader :amazon_images
87
- @@amazon_images = YAML.load(File.read("#{MU.myRoot}/modules/mu/defaults/amazon_images.yaml"))
88
- if File.exists?("#{MU.etcDir}/amazon_images.yaml")
89
- custom = YAML.load(File.read("#{MU.etcDir}/amazon_images.yaml"))
90
- @@amazon_images.merge!(custom) { |key, oldval, newval|
91
- if !oldval.is_a?(Hash) and !newval.nil?
92
- if !newval.nil?
93
- newval
94
- else
95
- oldval
96
- end
97
- else
98
- oldval.merge(newval)
99
- end
100
- }
101
- end
102
- # The list of known Amazon AMIs, by region, which we can use for a given
103
- # platform.
104
- def self.amazon_images
105
- @@amazon_images
106
- end
107
-
108
70
  @@config_path = nil
109
71
  # The path to the most recently loaded configuration file
110
72
  attr_reader :config_path
@@ -123,20 +85,24 @@ module MU
123
85
  def self.schemaMerge(orig, new, cloud)
124
86
  if new.is_a?(Hash)
125
87
  new.each_pair { |k, v|
88
+ if cloud and k == "description" and v.is_a?(String) and !v.match(/\b#{Regexp.quote(cloud.upcase)}\b/) and !v.empty?
89
+ new[k] = "+"+cloud.upcase+"+: "+v
90
+ end
126
91
  if orig and orig.has_key?(k)
127
- schemaMerge(orig[k], new[k], cloud)
128
92
  elsif orig
129
93
  orig[k] = new[k]
130
94
  else
131
95
  orig = new
132
96
  end
97
+ schemaMerge(orig[k], new[k], cloud)
133
98
  }
134
99
  elsif orig.is_a?(Array) and new
135
100
  orig.concat(new)
136
101
  orig.uniq!
137
102
  elsif new.is_a?(String)
138
103
  orig ||= ""
139
- orig += "\n#{cloud.upcase}: "+new
104
+ orig += "\n" if !orig.empty?
105
+ orig += "+#{cloud.upcase}+: "+new
140
106
  else
141
107
  # XXX I think this is a NOOP?
142
108
  end
@@ -171,8 +137,6 @@ module MU
171
137
  # recursively chase down description fields in arrays and objects of our
172
138
  # schema and prepend stuff to them for documentation
173
139
  def self.prepend_descriptions(prefix, cfg)
174
- # cfg["description"] ||= ""
175
- # cfg["description"] = prefix+cfg["description"]
176
140
  cfg["prefix"] = prefix
177
141
  if cfg["type"] == "array" and cfg["items"]
178
142
  cfg["items"] = prepend_descriptions(prefix, cfg["items"])
@@ -196,7 +160,9 @@ module MU
196
160
  next if required.size == 0 and res_schema.size == 0
197
161
  res_schema.each { |key, cfg|
198
162
  cfg["description"] ||= ""
199
- cfg["description"] = "+"+cloud.upcase+"+: "+cfg["description"]
163
+ if !cfg["description"].empty?
164
+ cfg["description"] = "\n# +"+cloud.upcase+"+: "+cfg["description"]
165
+ end
200
166
  if docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]
201
167
  schemaMerge(docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key], cfg, cloud)
202
168
  docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]["description"] ||= ""
@@ -205,6 +171,7 @@ module MU
205
171
  else
206
172
  if only_children[attrs[:cfg_plural]][key]
207
173
  prefix = only_children[attrs[:cfg_plural]][key].keys.map{ |x| x.upcase }.join(" & ")+" ONLY"
174
+ cfg["description"].gsub!(/^\n#/, '') # so we don't leave the description blank in the "optional parameters" section
208
175
  cfg = prepend_descriptions(prefix, cfg)
209
176
  end
210
177
 
@@ -247,19 +214,334 @@ module MU
247
214
  # @return [Hash]: The modified configuration
248
215
  def self.manxify(config)
249
216
  if config.is_a?(Hash)
217
+ newhash = {}
250
218
  config.each_pair { |key, val|
251
- config[key] = self.manxify(val)
219
+ newhash[key] = self.manxify(val)
252
220
  }
221
+ config = newhash
253
222
  elsif config.is_a?(Array)
223
+ newarray = []
254
224
  config.each { |val|
255
- val = self.manxify(val)
225
+ newarray << self.manxify(val)
256
226
  }
227
+ config = newarray
257
228
  elsif config.is_a?(MU::Config::Tail)
258
229
  return config.to_s
230
+ elsif config.is_a?(MU::Config::Ref)
231
+ return config.to_h
259
232
  end
260
233
  return config
261
234
  end
262
235
 
236
+ # Make a deep copy of a config hash and pare it down to only primitive
237
+ # types, even at the leaves.
238
+ # @param config [Hash]
239
+ # @return [Hash]
240
+ def self.stripConfig(config)
241
+ MU::Config.manxify(Marshal.load(Marshal.dump(MU.structToHash(config.dup))))
242
+ end
243
+
244
+ # A wrapper class for resources to refer to other resources, whether they
245
+ # be a sibling object in the current deploy, an object in another deploy,
246
+ # or a plain cloud id from outside of Mu.
247
+ class Ref
248
+ attr_reader :name
249
+ attr_reader :type
250
+ attr_reader :cloud
251
+ attr_reader :deploy_id
252
+ attr_reader :region
253
+ attr_reader :credentials
254
+ attr_reader :habitat
255
+ attr_reader :mommacat
256
+ attr_reader :tag_key
257
+ attr_reader :tag_value
258
+ attr_reader :obj
259
+
260
+ @@refs = []
261
+ @@ref_semaphore = Mutex.new
262
+
263
+ # Little bit of a factory pattern... given a hash of options for a {MU::Config::Ref} objects, first see if we have an existing one that matches our more immutable attributes (+cloud+, +id+, etc). If we do, return that. If we do not, create one, add that to our inventory, and return that instead.
264
+ # @param cfg [Hash]:
265
+ # @return [MU::Config::Ref]
266
+ def self.get(cfg)
267
+ return cfg if cfg.is_a?(MU::Config::Ref)
268
+ checkfields = cfg.keys.map { |k| k.to_sym }
269
+ required = [:id, :type]
270
+
271
+ @@ref_semaphore.synchronize {
272
+ match = nil
273
+ @@refs.each { |ref|
274
+ saw_mismatch = false
275
+ saw_match = false
276
+ needed_values = []
277
+ checkfields.each { |field|
278
+ next if !cfg[field]
279
+ ext_value = ref.instance_variable_get("@#{field.to_s}".to_sym)
280
+ if !ext_value
281
+ needed_values << field
282
+ next
283
+ end
284
+ if cfg[field] != ext_value
285
+ saw_mismatch = true
286
+ elsif required.include?(field) and cfg[field] == ext_value
287
+ saw_match = true
288
+ end
289
+ }
290
+ if saw_match and !saw_mismatch
291
+ # populate empty fields we got from this request
292
+ if needed_values.size > 0
293
+ newref = ref.dup
294
+ needed_values.each { |field|
295
+ newref.instance_variable_set("@#{field.to_s}".to_sym, cfg[field])
296
+ if !newref.respond_to?(field)
297
+ newref.singleton_class.instance_eval { attr_reader field.to_sym }
298
+ end
299
+ }
300
+ @@refs << newref
301
+ return newref
302
+ else
303
+ return ref
304
+ end
305
+ end
306
+ }
307
+
308
+ }
309
+
310
+ # if we get here, there was no match
311
+ newref = MU::Config::Ref.new(cfg)
312
+ @@ref_semaphore.synchronize {
313
+ @@refs << newref
314
+ return newref
315
+ }
316
+ end
317
+
318
+ # @param cfg [Hash]: A Basket of Kittens configuration hash containing
319
+ # lookup information for a cloud object
320
+ def initialize(cfg)
321
+ cfg.keys.each { |field|
322
+ next if field == "tag"
323
+ if !cfg[field].nil?
324
+ self.instance_variable_set("@#{field}".to_sym, cfg[field])
325
+ elsif !cfg[field.to_sym].nil?
326
+ self.instance_variable_set("@#{field.to_s}".to_sym, cfg[field.to_sym])
327
+ end
328
+ self.singleton_class.instance_eval { attr_reader field.to_sym }
329
+ }
330
+ if cfg['tag'] and cfg['tag']['key'] and
331
+ !cfg['tag']['key'].empty? and cfg['tag']['value']
332
+ @tag_key = cfg['tag']['key']
333
+ @tag_value = cfg['tag']['value']
334
+ end
335
+
336
+ if @deploy_id and !@mommacat
337
+ @mommacat = MU::MommaCat.new(@deploy_id, set_context_to_me: false, create: false)
338
+ elsif @mommacat and !@deploy_id
339
+ @deploy_id = @mommacat.deploy_id
340
+ end
341
+
342
+ kitten if @mommacat # try to populate the actual cloud object for this
343
+ end
344
+
345
+ # Comparison operator
346
+ def <=>(other)
347
+ return 1 if other.nil?
348
+ self.to_s <=> other.to_s
349
+ end
350
+
351
+ # Base configuration schema for declared kittens referencing other cloud objects. This is essentially a set of filters that we're going to pass to {MU::MommaCat.findStray}.
352
+ # @param aliases [Array<Hash>]: Key => value mappings to set backwards-compatibility aliases for attributes, such as the ubiquitous +vpc_id+ (+vpc_id+ => +id+).
353
+ # @return [Hash]
354
+ def self.schema(aliases = [], type: nil, parent_obj: nil, desc: nil)
355
+ parent_obj ||= caller[1].gsub(/.*?\/([^\.\/]+)\.rb:.*/, '\1')
356
+ desc ||= "Reference a #{type ? "'#{type}' resource" : "resource" } from this #{parent_obj ? "'#{parent_obj}'" : "" } resource"
357
+ schema = {
358
+ "type" => "object",
359
+ "#MU_REFERENCE" => true,
360
+ "minProperties" => 1,
361
+ "description" => desc,
362
+ "properties" => {
363
+ "id" => {
364
+ "type" => "string",
365
+ "description" => "Cloud identifier of a resource we want to reference, typically used when leveraging resources not managed by MU"
366
+ },
367
+ "name" => {
368
+ "type" => "string",
369
+ "description" => "The short (internal Mu) name of a resource we're attempting to reference. Typically used when referring to a sibling resource elsewhere in the same deploy, or in another known Mu deploy in conjunction with +deploy_id+."
370
+ },
371
+ "type" => {
372
+ "type" => "string",
373
+ "description" => "The resource type we're attempting to reference.",
374
+ "enum" => MU::Cloud.resource_types.values.map { |t| t[:cfg_plural] }
375
+ },
376
+ "deploy_id" => {
377
+ "type" => "string",
378
+ "description" => "Our target resource should be found in this Mu deploy."
379
+ },
380
+ "credentials" => MU::Config.credentials_primitive,
381
+ "region" => MU::Config.region_primitive,
382
+ "cloud" => MU::Config.cloud_primitive,
383
+ "tag" => {
384
+ "type" => "object",
385
+ "description" => "If the target resource supports tagging and our resource implementations +find+ method supports it, we can attempt to locate it by tag.",
386
+ "properties" => {
387
+ "key" => {
388
+ "type" => "string",
389
+ "description" => "The tag or label key to search against"
390
+ },
391
+ "value" => {
392
+ "type" => "string",
393
+ "description" => "The tag or label value to match"
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ if !["folders", "habitats"].include?(type)
400
+ schema["properties"]["habitat"] = MU::Config::Habitat.reference
401
+ end
402
+
403
+ if !type.nil?
404
+ schema["required"] = ["type"]
405
+ schema["properties"]["type"]["default"] = type
406
+ schema["properties"]["type"]["enum"] = [type]
407
+ end
408
+
409
+ aliases.each { |a|
410
+ a.each_pair { |k, v|
411
+ if schema["properties"][v]
412
+ schema["properties"][k] = schema["properties"][v].dup
413
+ schema["properties"][k]["description"] = "Alias for <tt>#{v}</tt>"
414
+ else
415
+ MU.log "Reference schema alias #{k} wants to alias #{v}, but no such attribute exists", MU::WARN, details: caller[4]
416
+ end
417
+ }
418
+ }
419
+
420
+ schema
421
+ end
422
+
423
+ # Decompose into a plain-jane {MU::Config::BasketOfKittens} hash fragment,
424
+ # of the sort that would have been used to declare this reference in the
425
+ # first place.
426
+ def to_h
427
+ me = { }
428
+
429
+ self.instance_variables.each { |var|
430
+ next if [:@obj, :@mommacat, :@tag_key, :@tag_value].include?(var)
431
+ val = self.instance_variable_get(var)
432
+ next if val.nil?
433
+ val = val.to_h if val.is_a?(MU::Config::Ref)
434
+ me[var.to_s.sub(/^@/, '')] = val
435
+ }
436
+ if @tag_key and !@tag_key.empty?
437
+ me['tag'] = {
438
+ 'key' => @tag_key,
439
+ 'value' => @tag_value
440
+ }
441
+ end
442
+ me
443
+ end
444
+
445
+ # Getter for the #{id} instance variable that attempts to populate it if
446
+ # it's not set.
447
+ # @return [String,nil]
448
+ def id
449
+ return @id if @id
450
+ kitten # if it's not defined, attempt to define it
451
+ @id
452
+ end
453
+
454
+ # Alias for {id}
455
+ # @return [String,nil]
456
+ def cloud_id
457
+ id
458
+ end
459
+
460
+ # Return a {MU::Cloud} object for this reference. This is only meant to be
461
+ # called in a live deploy, which is to say that if called during initial
462
+ # configuration parsing, results may be incorrect.
463
+ # @param mommacat [MU::MommaCat]: A deploy object which will be searched for the referenced resource if provided, before restoring to broader, less efficient searches.
464
+ def kitten(mommacat = @mommacat)
465
+ return nil if !@cloud or !@type
466
+
467
+ if @obj
468
+ @deploy_id ||= @obj.deploy_id
469
+ @id ||= @obj.cloud_id
470
+ @name ||= @obj.config['name']
471
+ return @obj
472
+ end
473
+
474
+ if mommacat
475
+ @obj = mommacat.findLitterMate(type: @type, name: @name, cloud_id: @id, credentials: @credentials, debug: false)
476
+ if @obj # initialize missing attributes, if we can
477
+ @id ||= @obj.cloud_id
478
+ @mommacat ||= mommacat
479
+ @obj.intoDeploy(@mommacat) # make real sure these are set
480
+ @deploy_id ||= mommacat.deploy_id
481
+ if !@name
482
+ if @obj.config and @obj.config['name']
483
+ @name = @obj.config['name']
484
+ elsif @obj.mu_name
485
+ if @type == "folders"
486
+ MU.log "would assign name '#{@obj.mu_name}' in ref to this folder if I were feeling aggressive", MU::WARN, details: self.to_h
487
+ end
488
+ # @name = @obj.mu_name
489
+ end
490
+ end
491
+ return @obj
492
+ else
493
+ # MU.log "Failed to find a live '#{@type.to_s}' object named #{@name}#{@id ? " (#{@id})" : "" }#{ @habitat ? " in habitat #{@habitat}" : "" }", MU::WARN, details: self
494
+ end
495
+ end
496
+
497
+ if !@obj and !(@cloud == "Google" and @id and @type == "users" and MU::Cloud::Google::User.cannedServiceAcctName?(@id))
498
+
499
+ begin
500
+ hab_arg = if @habitat.nil?
501
+ [nil]
502
+ elsif @habitat.is_a?(MU::Config::Ref)
503
+ [@habitat.id]
504
+ elsif @habitat.is_a?(Hash)
505
+ [@habitat["id"]]
506
+ else
507
+ [@habitat.to_s]
508
+ end
509
+
510
+ found = MU::MommaCat.findStray(
511
+ @cloud,
512
+ @type,
513
+ name: @name,
514
+ cloud_id: @id,
515
+ deploy_id: @deploy_id,
516
+ region: @region,
517
+ habitats: hab_arg,
518
+ credentials: @credentials,
519
+ dummy_ok: (["habitats", "folders", "users", "groups"].include?(@type))
520
+ )
521
+ @obj ||= found.first if found
522
+ rescue ThreadError => e
523
+ # Sometimes MommaCat calls us in a potential deadlock situation;
524
+ # don't be the cause of a fatal error if so, we don't need this
525
+ # object that badly.
526
+ raise e if !e.message.match(/recursive locking/)
527
+ rescue SystemExit => e
528
+ # XXX this is temporary, to cope with some debug stuff that's in findStray
529
+ # for the nonce
530
+ return
531
+ end
532
+ end
533
+
534
+ if @obj
535
+ @deploy_id ||= @obj.deploy_id
536
+ @id ||= @obj.cloud_id
537
+ @name ||= @obj.config['name']
538
+ end
539
+
540
+ @obj
541
+ end
542
+
543
+ end
544
+
263
545
  # A wrapper for config leaves that came from ERB parameters instead of raw
264
546
  # YAML or JSON. Will behave like a string for things that expect that
265
547
  # sort of thing. Code that needs to know that this leaf was the result of
@@ -324,7 +606,7 @@ module MU
324
606
  end
325
607
  # Walk like a String
326
608
  def to_s
327
- @prefix+@value+@suffix
609
+ @prefix.to_s+@value.to_s+@suffix.to_s
328
610
  end
329
611
  # Quack like a String
330
612
  def to_str
@@ -350,6 +632,11 @@ module MU
350
632
  def ==(o)
351
633
  (o.class == self.class or o.class == "String") && o.to_s == to_s
352
634
  end
635
+ # Concatenate like a string
636
+ def +(o)
637
+ return to_s if o.nil?
638
+ to_s + o.to_s
639
+ end
353
640
  # Perform global substitutions like a String
354
641
  def gsub(*args)
355
642
  to_s.gsub(*args)
@@ -437,6 +724,7 @@ module MU
437
724
  "MU::Config.getTail PLACEHOLDER #{var_name} REDLOHECALP"
438
725
  else
439
726
  tail = getTail(var_name.to_s)
727
+
440
728
  if tail.is_a?(Array)
441
729
  if @param_pass
442
730
  return tail.map {|f| f.values.first.to_s }.join(",")
@@ -446,7 +734,11 @@ module MU
446
734
  return "MU::Config.getTail PLACEHOLDER #{var_name} REDLOHECALP"
447
735
  end
448
736
  else
449
- return "MU::Config.getTail PLACEHOLDER #{var_name} REDLOHECALP"
737
+ if @param_pass
738
+ tail.to_s
739
+ else
740
+ return "MU::Config.getTail PLACEHOLDER #{var_name} REDLOHECALP"
741
+ end
450
742
  end
451
743
  end
452
744
  end
@@ -470,13 +762,28 @@ module MU
470
762
  "MU::Config.getTail PLACEHOLDER #{var_name} REDLOHECALP"
471
763
  end
472
764
 
765
+ # Make sure our parameter values are all available in the local namespace
766
+ # that ERB will be using, minus any that conflict with existing variables
767
+ erb_binding = get_binding
768
+ @@tails.each_pair { |key, tail|
769
+ next if !tail.is_a?(MU::Config::Tail) or tail.is_list_element
770
+ # XXX figure out what to do with lists
771
+ begin
772
+ erb_binding.local_variable_set(key.to_sym, tail.to_s)
773
+ rescue NameError
774
+ MU.log "Binding #{key} = #{tail.to_s}", MU::DEBUG
775
+ erb_binding.local_variable_set(key.to_sym, tail.to_s)
776
+ end
777
+ }
778
+
473
779
  # Figure out what kind of file we're loading. We handle includes
474
780
  # differently if YAML is involved. These globals get used inside
475
781
  # templates. They're globals on purpose. Stop whining.
476
782
  $file_format = MU::Config.guessFormat(path)
477
783
  $yaml_refs = {}
478
784
  erb = ERB.new(File.read(path), nil, "<>")
479
- raw_text = erb.result(get_binding)
785
+
786
+ raw_text = erb.result(erb_binding)
480
787
  raw_json = nil
481
788
 
482
789
  # If we're working in YAML, do some magic to make includes work better.
@@ -533,7 +840,7 @@ module MU
533
840
  # @param skipinitialupdates [Boolean]: Whether to forcibly apply the *skipinitialupdates* flag to nodes created by this configuration.
534
841
  # @param params [Hash]: Optional name-value parameter pairs, which will be passed to our configuration files as ERB variables.
535
842
  # @return [Hash]: The complete validated configuration for a deployment.
536
- def initialize(path, skipinitialupdates = false, params: params = Hash.new, updating: nil)
843
+ def initialize(path, skipinitialupdates = false, params: {}, updating: nil, default_credentials: nil)
537
844
  $myPublicIp = MU::Cloud::AWS.getAWSMetaData("public-ipv4")
538
845
  $myRoot = MU.myRoot
539
846
  $myRoot.freeze
@@ -551,6 +858,7 @@ module MU
551
858
  @admin_firewall_rules = []
552
859
  @skipinitialupdates = skipinitialupdates
553
860
  @updating = updating
861
+ @default_credentials = default_credentials
554
862
 
555
863
  ok = true
556
864
  params.each_pair { |name, value|
@@ -601,15 +909,19 @@ module MU
601
909
  elsif param["required"] or !param.has_key?("required")
602
910
  MU.log "Required parameter '#{param['name']}' not supplied", MU::ERR
603
911
  ok = false
604
- end
605
- if param.has_key?("cloudtype")
606
- getTail(param['name'], value: @@parameters[param['name']], cloudtype: param["cloudtype"], valid_values: param['valid_values'], description: param['description'], prettyname: param['prettyname'], list_of: param['list_of'])
607
- else
608
- getTail(param['name'], value: @@parameters[param['name']], valid_values: param['valid_values'], description: param['description'], prettyname: param['prettyname'], list_of: param['list_of'])
912
+ next
913
+ else # not required, no default
914
+ next
609
915
  end
610
916
  end
917
+ if param.has_key?("cloudtype")
918
+ getTail(param['name'], value: @@parameters[param['name']], cloudtype: param["cloudtype"], valid_values: param['valid_values'], description: param['description'], prettyname: param['prettyname'], list_of: param['list_of'])
919
+ else
920
+ getTail(param['name'], value: @@parameters[param['name']], valid_values: param['valid_values'], description: param['description'], prettyname: param['prettyname'], list_of: param['list_of'])
921
+ end
611
922
  }
612
923
  end
924
+
613
925
  raise ValidationError if !ok
614
926
  @@parameters.each_pair { |name, val|
615
927
  next if @@tails.has_key?(name) and @@tails[name].is_a?(MU::Config::Tail) and @@tails[name].pseudo
@@ -626,13 +938,13 @@ module MU
626
938
  MU.log "Passing variable '#{name}' into #{path} with value '#{val}'"
627
939
  }
628
940
  raise DeployParamError, "One or more invalid parameters specified" if !ok
629
- $parameters = @@parameters
941
+ $parameters = @@parameters.dup
630
942
  $parameters.freeze
631
943
 
632
944
  tmp_cfg, raw_erb = resolveConfig(path: @@config_path)
633
945
 
634
946
  # Convert parameter entries that constitute whole config keys into
635
- # MU::Config::Tail objects.
947
+ # {MU::Config::Tail} objects.
636
948
  def resolveTails(tree, indent= "")
637
949
  if tree.is_a?(Hash)
638
950
  tree.each_pair { |key, val|
@@ -663,19 +975,32 @@ module MU
663
975
  }
664
976
  ]
665
977
  end
666
- MU::Config.set_defaults(@config, MU::Config.schema)
978
+
979
+ @config['credentials'] ||= @default_credentials
980
+
981
+ types = MU::Cloud.resource_types.values.map { |v| v[:cfg_plural] }
982
+
983
+ MU::Cloud.resource_types.values.map { |v| v[:cfg_plural] }.each { |type|
984
+ if @config[type]
985
+ @config[type].each { |k|
986
+ applyInheritedDefaults(k, type)
987
+ }
988
+ end
989
+ }
990
+ applySchemaDefaults(@config, MU::Config.schema)
991
+
667
992
  validate # individual resources validate when added now, necessary because the schema can change depending on what cloud they're targeting
668
993
  # XXX but now we're not validating top-level keys, argh
669
994
  #pp @config
670
995
  #raise "DERP"
671
- return @config.freeze
996
+ @config.freeze
672
997
  end
673
998
 
674
999
  # Output the dependencies of this BoK stack as a directed acyclic graph.
675
1000
  # Very useful for debugging.
676
1001
  def visualizeDependencies
677
1002
  # GraphViz won't like MU::Config::Tail, pare down to plain Strings
678
- config = MU::Config.manxify(Marshal.load(Marshal.dump(@config)))
1003
+ config = MU::Config.stripConfig(@config)
679
1004
  begin
680
1005
  g = GraphViz.new(:G, :type => :digraph)
681
1006
  # Generate a GraphViz node for each resource in this stack
@@ -781,7 +1106,9 @@ module MU
781
1106
  shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
782
1107
  if @kittens[cfg_plural]
783
1108
  @kittens[cfg_plural].each { |kitten|
784
- if kitten['name'] == name.to_s or kitten['virtual_name'] == name.to_s
1109
+ if kitten['name'].to_s == name.to_s or
1110
+ kitten['virtual_name'].to_s == name.to_s or
1111
+ (has_multiple and name.nil?)
785
1112
  if has_multiple
786
1113
  matches << kitten
787
1114
  else
@@ -857,11 +1184,18 @@ module MU
857
1184
  # @param descriptor [Hash]: The configuration description, as from a Basket of Kittens
858
1185
  # @param type [String]: The type of resource being added
859
1186
  # @param delay_validation [Boolean]: Whether to hold off on calling the resource's validateConfig method
860
- def insertKitten(descriptor, type, delay_validation = false)
1187
+ # @param ignore_duplicates [Boolean]: Do not raise an exception if we attempt to insert a resource with a +name+ field that's already in use
1188
+ def insertKitten(descriptor, type, delay_validation = false, ignore_duplicates: false)
861
1189
  append = false
1190
+ start = Time.now
1191
+ shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
1192
+
1193
+ if !ignore_duplicates and haveLitterMate?(descriptor['name'], cfg_name)
1194
+ # raise DuplicateNameError, "A #{shortclass} named #{descriptor['name']} has already been inserted into this configuration"
1195
+ end
862
1196
 
863
1197
  @kittencfg_semaphore.synchronize {
864
- append = !@kittens[type].include?(descriptor)
1198
+ append = !@kittens[cfg_plural].include?(descriptor)
865
1199
 
866
1200
  # Skip if this kitten has already been validated and appended
867
1201
  if !append and descriptor["#MU_VALIDATED"]
@@ -870,10 +1204,26 @@ module MU
870
1204
  }
871
1205
  ok = true
872
1206
 
873
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
874
1207
  descriptor["#MU_CLOUDCLASS"] = classname
875
- inheritDefaults(descriptor, cfg_plural)
1208
+
1209
+ applyInheritedDefaults(descriptor, cfg_plural)
1210
+
1211
+ # Meld defaults from our global schema and, if applicable, from our
1212
+ # cloud-specific schema.
876
1213
  schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
1214
+ myschema = Marshal.load(Marshal.dump(MU::Config.schema["properties"][cfg_plural]["items"]))
1215
+ more_required, more_schema = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s).schema(self)
1216
+ if more_schema
1217
+ MU::Config.schemaMerge(myschema["properties"], more_schema, descriptor["cloud"])
1218
+ end
1219
+ myschema["required"] ||= []
1220
+ if more_required
1221
+ myschema["required"].concat(more_required)
1222
+ myschema["required"].uniq!
1223
+ end
1224
+
1225
+ descriptor = applySchemaDefaults(descriptor, myschema, type: shortclass)
1226
+ MU.log "Schema check on #{descriptor['cloud']} #{cfg_name} #{descriptor['name']}", MU::DEBUG, details: myschema
877
1227
 
878
1228
  if (descriptor["region"] and descriptor["region"].empty?) or
879
1229
  (descriptor['cloud'] == "Google" and ["firewall_rule", "vpc"].include?(cfg_name))
@@ -881,8 +1231,8 @@ module MU
881
1231
  end
882
1232
 
883
1233
  # Make sure a sensible region has been targeted, if applicable
1234
+ classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"])
884
1235
  if descriptor["region"]
885
- classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"])
886
1236
  valid_regions = classobj.listRegions
887
1237
  if !valid_regions.include?(descriptor["region"])
888
1238
  MU.log "Known regions for cloud '#{descriptor['cloud']}' do not include '#{descriptor["region"]}'", MU::ERR, details: valid_regions
@@ -890,8 +1240,10 @@ module MU
890
1240
  end
891
1241
  end
892
1242
 
893
- if descriptor['project']
894
- if haveLitterMate?(descriptor['project'], "habitats")
1243
+ if descriptor.has_key?('project')
1244
+ if descriptor['project'].nil?
1245
+ descriptor.delete('project')
1246
+ elsif haveLitterMate?(descriptor['project'], "habitats")
895
1247
  descriptor['dependencies'] ||= []
896
1248
  descriptor['dependencies'] << {
897
1249
  "type" => "habitat",
@@ -902,6 +1254,16 @@ module MU
902
1254
 
903
1255
  # Does this resource go in a VPC?
904
1256
  if !descriptor["vpc"].nil? and !delay_validation
1257
+ # Quietly fix old vpc reference style
1258
+ if descriptor['vpc']['vpc_id']
1259
+ descriptor['vpc']['id'] ||= descriptor['vpc']['vpc_id']
1260
+ descriptor['vpc'].delete('vpc_id')
1261
+ end
1262
+ if descriptor['vpc']['vpc_name']
1263
+ descriptor['vpc']['name'] = descriptor['vpc']['vpc_name']
1264
+ descriptor['vpc'].delete('vpc_name')
1265
+ end
1266
+
905
1267
  descriptor['vpc']['cloud'] = descriptor['cloud']
906
1268
  if descriptor['credentials']
907
1269
  descriptor['vpc']['credentials'] ||= descriptor['credentials']
@@ -911,16 +1273,27 @@ module MU
911
1273
  end
912
1274
 
913
1275
  # If we're using a VPC in this deploy, set it as a dependency
914
- if !descriptor["vpc"]["vpc_name"].nil? and
915
- haveLitterMate?(descriptor["vpc"]["vpc_name"], "vpcs") and
1276
+ if !descriptor["vpc"]["name"].nil? and
1277
+ haveLitterMate?(descriptor["vpc"]["name"], "vpcs") and
916
1278
  descriptor["vpc"]['deploy_id'].nil? and
917
- descriptor["vpc"]['vpc_id'].nil?
1279
+ descriptor["vpc"]['id'].nil?
918
1280
  descriptor["dependencies"] << {
919
1281
  "type" => "vpc",
920
- "name" => descriptor["vpc"]["vpc_name"]
1282
+ "name" => descriptor["vpc"]["name"],
921
1283
  }
1284
+ siblingvpc = haveLitterMate?(descriptor["vpc"]["name"], "vpcs")
1285
+
1286
+ if siblingvpc and siblingvpc['bastion'] and
1287
+ ["server", "server_pool"].include?(cfg_name) and
1288
+ !descriptor['bastion']
1289
+ if descriptor['name'] != siblingvpc['bastion'].to_h['name']
1290
+ descriptor["dependencies"] << {
1291
+ "type" => "server",
1292
+ "name" => siblingvpc['bastion'].to_h['name']
1293
+ }
1294
+ end
1295
+ end
922
1296
 
923
- siblingvpc = haveLitterMate?(descriptor["vpc"]["vpc_name"], "vpcs")
924
1297
  # things that live in subnets need their VPCs to be fully
925
1298
  # resolved before we can proceed
926
1299
  if ["server", "server_pool", "loadbalancer", "database", "cache_cluster", "container_cluster", "storage_pool"].include?(cfg_name)
@@ -930,11 +1303,11 @@ module MU
930
1303
  end
931
1304
  if !MU::Config::VPC.processReference(descriptor['vpc'],
932
1305
  cfg_plural,
933
- shortclass.to_s+" '#{descriptor['name']}'",
1306
+ descriptor,
934
1307
  self,
935
1308
  dflt_region: descriptor['region'],
936
- is_sibling: true,
937
1309
  credentials: descriptor['credentials'],
1310
+ dflt_project: descriptor['project'],
938
1311
  sibling_vpcs: @kittens['vpcs'])
939
1312
  ok = false
940
1313
  end
@@ -943,12 +1316,13 @@ module MU
943
1316
  # thing exists, and also fetch its id now so later search routines
944
1317
  # don't have to work so hard.
945
1318
  else
946
- if !MU::Config::VPC.processReference(descriptor["vpc"], cfg_plural,
947
- "#{shortclass} #{descriptor['name']}",
1319
+ if !MU::Config::VPC.processReference(descriptor["vpc"],
1320
+ cfg_plural,
1321
+ descriptor,
948
1322
  self,
949
1323
  credentials: descriptor['credentials'],
1324
+ dflt_project: descriptor['project'],
950
1325
  dflt_region: descriptor['region'])
951
- MU.log "insertKitten was called from #{caller[0]}", MU::ERR
952
1326
  ok = false
953
1327
  end
954
1328
  end
@@ -979,6 +1353,7 @@ module MU
979
1353
  (descriptor['ingress_rules'] or
980
1354
  ["server", "server_pool", "database"].include?(cfg_name))
981
1355
  descriptor['ingress_rules'] ||= []
1356
+ fw_classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get("FirewallRule")
982
1357
 
983
1358
  acl = {
984
1359
  "name" => fwname,
@@ -986,14 +1361,24 @@ module MU
986
1361
  "region" => descriptor['region'],
987
1362
  "credentials" => descriptor["credentials"]
988
1363
  }
989
- acl["vpc"] = descriptor['vpc'].dup if descriptor['vpc']
1364
+ if !fw_classobj.isGlobal?
1365
+ acl['region'] = descriptor['region']
1366
+ acl['region'] ||= classobj.myRegion(acl['credentials'])
1367
+ else
1368
+ acl.delete("region")
1369
+ end
1370
+ if descriptor["vpc"]
1371
+ acl["vpc"] = descriptor['vpc'].dup
1372
+ acl["vpc"].delete("subnet_pref")
1373
+ end
1374
+
990
1375
  ["optional_tags", "tags", "cloud", "project"].each { |param|
991
1376
  acl[param] = descriptor[param] if descriptor[param]
992
1377
  }
993
1378
  descriptor["add_firewall_rules"] = [] if descriptor["add_firewall_rules"].nil?
994
- descriptor["add_firewall_rules"] << {"rule_name" => fwname}
1379
+ descriptor["add_firewall_rules"] << {"rule_name" => fwname, "type" => "firewall_rules" } # XXX why the duck is there a type argument required here?
995
1380
  acl = resolveIntraStackFirewallRefs(acl)
996
- ok = false if !insertKitten(acl, "firewall_rules")
1381
+ ok = false if !insertKitten(acl, "firewall_rules", delay_validation)
997
1382
  end
998
1383
 
999
1384
  # Does it declare association with any sibling LoadBalancers?
@@ -1030,7 +1415,7 @@ module MU
1030
1415
  }
1031
1416
  siblingfw = haveLitterMate?(acl_include["rule_name"], "firewall_rules")
1032
1417
  if !siblingfw["#MU_VALIDATED"]
1033
- ok = false if !insertKitten(siblingfw, "firewall_rules")
1418
+ ok = false if !insertKitten(siblingfw, "firewall_rules", delay_validation)
1034
1419
  end
1035
1420
  elsif acl_include["rule_name"]
1036
1421
  MU.log shortclass.to_s+" #{descriptor['name']} depends on FirewallRule #{acl_include["rule_name"]}, but no such rule declared.", MU::ERR
@@ -1103,21 +1488,9 @@ module MU
1103
1488
  # here
1104
1489
  ok = false if !schemaclass.validate(descriptor, self)
1105
1490
 
1106
- # Merge the cloud-specific JSON schema and validate against it
1107
- myschema = Marshal.load(Marshal.dump(MU::Config.schema["properties"][cfg_plural]["items"]))
1108
- more_required, more_schema = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s).schema(self)
1109
-
1110
- if more_schema
1111
- MU::Config.schemaMerge(myschema["properties"], more_schema, descriptor["cloud"])
1112
- MU::Config.set_defaults(descriptor, myschema)
1113
- end
1114
- myschema["required"] ||= []
1115
- myschema["required"].concat(more_required)
1116
- myschema["required"].uniq!
1117
- MU.log "Schema check on #{descriptor['cloud']} #{cfg_name} #{descriptor['name']}", MU::DEBUG, details: myschema
1118
-
1119
- plain_cfg = MU::Config.manxify(Marshal.load(Marshal.dump(descriptor)))
1491
+ plain_cfg = MU::Config.stripConfig(descriptor)
1120
1492
  plain_cfg.delete("#MU_CLOUDCLASS")
1493
+ plain_cfg.delete("#MU_VALIDATION_ATTEMPTED")
1121
1494
  plain_cfg.delete("#TARGETCLASS")
1122
1495
  plain_cfg.delete("#TARGETNAME")
1123
1496
  plain_cfg.delete("parent_block") if cfg_plural == "vpcs"
@@ -1143,17 +1516,23 @@ module MU
1143
1516
  # on stuff that will cause spurious alarms further in
1144
1517
  if ok
1145
1518
  parser = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s)
1146
- plain_descriptor = MU::Config.manxify(Marshal.load(Marshal.dump(descriptor)))
1147
- passed = parser.validateConfig(plain_descriptor, self)
1519
+ original_descriptor = MU::Config.stripConfig(descriptor)
1520
+ passed = parser.validateConfig(descriptor, self)
1148
1521
 
1149
- if passed
1150
- descriptor.merge!(plain_descriptor)
1151
- else
1522
+ if !passed
1523
+ descriptor = original_descriptor
1152
1524
  ok = false
1153
1525
  end
1526
+
1527
+ # Make sure we've been configured with the right credentials
1528
+ cloudbase = Object.const_get("MU").const_get("Cloud").const_get(descriptor['cloud'])
1529
+ credcfg = cloudbase.credConfig(descriptor['credentials'])
1530
+ if !credcfg or credcfg.empty?
1531
+ raise ValidationError, "#{descriptor['cloud']} #{cfg_name} #{descriptor['name']} declares credential set #{descriptor['credentials']}, but no such credentials exist for that cloud provider"
1532
+ end
1533
+
1154
1534
  descriptor['#MU_VALIDATED'] = true
1155
1535
  end
1156
-
1157
1536
  end
1158
1537
 
1159
1538
  descriptor["dependencies"].uniq!
@@ -1161,13 +1540,15 @@ module MU
1161
1540
  @kittencfg_semaphore.synchronize {
1162
1541
  @kittens[cfg_plural] << descriptor if append
1163
1542
  }
1543
+
1164
1544
  ok
1165
1545
  end
1166
1546
 
1167
1547
  @@allregions = []
1168
- MU::Cloud.supportedClouds.each { |cloud|
1548
+ MU::Cloud.availableClouds.each { |cloud|
1169
1549
  cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
1170
- @@allregions.concat(cloudclass.listRegions())
1550
+ regions = cloudclass.listRegions()
1551
+ @@allregions.concat(regions) if regions
1171
1552
  }
1172
1553
 
1173
1554
  # Configuration chunk for choosing a provider region
@@ -1175,8 +1556,9 @@ module MU
1175
1556
  def self.region_primitive
1176
1557
  if !@@allregions or @@allregions.empty?
1177
1558
  @@allregions = []
1178
- MU::Cloud.supportedClouds.each { |cloud|
1559
+ MU::Cloud.availableClouds.each { |cloud|
1179
1560
  cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
1561
+ return @allregions if !cloudclass.listRegions()
1180
1562
  @@allregions.concat(cloudclass.listRegions())
1181
1563
  }
1182
1564
  end
@@ -1236,7 +1618,7 @@ module MU
1236
1618
  def self.cloud_primitive
1237
1619
  {
1238
1620
  "type" => "string",
1239
- "default" => MU::Config.defaultCloud,
1621
+ # "default" => MU::Config.defaultCloud, # applyInheritedDefaults does this better
1240
1622
  "enum" => MU::Cloud.supportedClouds
1241
1623
  }
1242
1624
  end
@@ -1250,7 +1632,7 @@ module MU
1250
1632
  # @param cloud [String]: The parent resource's cloud plugin identifier
1251
1633
  # @param region [String]: Cloud provider region, if applicable.
1252
1634
  # @return [Hash<String>]: A dependency description that the calling resource can then add to itself.
1253
- def adminFirewallRuleset(vpc: nil, admin_ip: nil, region: nil, cloud: nil, credentials: nil)
1635
+ def adminFirewallRuleset(vpc: nil, admin_ip: nil, region: nil, cloud: nil, credentials: nil, rules_only: false)
1254
1636
  if !cloud or (cloud == "AWS" and !region)
1255
1637
  raise MuError, "Cannot call adminFirewallRuleset without specifying the parent's region and cloud provider"
1256
1638
  end
@@ -1259,27 +1641,6 @@ module MU
1259
1641
  hosts << "#{MU.my_private_ip}/32" if MU.my_private_ip
1260
1642
  hosts << "#{MU.mu_public_ip}/32" if MU.mu_public_ip
1261
1643
  hosts << "#{admin_ip}/32" if admin_ip
1262
- hosts.uniq!
1263
- name = "admin"
1264
- name += credentials.to_s if credentials
1265
- realvpc = nil
1266
-
1267
- if vpc
1268
- realvpc = {}
1269
- realvpc['vpc_id'] = vpc['vpc_id'] if !vpc['vpc_id'].nil?
1270
- realvpc['vpc_name'] = vpc['vpc_name'] if !vpc['vpc_name'].nil?
1271
- realvpc['deploy_id'] = vpc['deploy_id'] if !vpc['deploy_id'].nil?
1272
- if !realvpc['vpc_id'].nil? and !realvpc['vpc_id'].empty?
1273
- # Stupid kludge for Google cloud_ids which are sometimes URLs and
1274
- # sometimes not. Requirements are inconsistent from scenario to
1275
- # scenario.
1276
- name = name + "-" + realvpc['vpc_id'].gsub(/.*\//, "")
1277
- realvpc['vpc_id'] = getTail("vpc_id", value: realvpc['vpc_id'], prettyname: "Admin Firewall Ruleset #{name} Target VPC", cloudtype: "AWS::EC2::VPC::Id") if realvpc["vpc_id"].is_a?(String)
1278
- elsif !realvpc['vpc_name'].nil?
1279
- name = name + "-" + realvpc['vpc_name']
1280
- end
1281
- end
1282
-
1283
1644
  hosts.uniq!
1284
1645
 
1285
1646
  rules = []
@@ -1296,9 +1657,43 @@ module MU
1296
1657
  ]
1297
1658
  end
1298
1659
 
1660
+ resclass = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get("FirewallRule")
1661
+
1662
+ if rules_only
1663
+ return rules
1664
+ end
1665
+
1666
+ name = "admin"
1667
+ name += credentials.to_s if credentials
1668
+ realvpc = nil
1669
+ if vpc
1670
+ realvpc = {}
1671
+ ['vpc_name', 'vpc_id'].each { |p|
1672
+ if vpc[p]
1673
+ vpc[p.sub(/^vpc_/, '')] = vpc[p]
1674
+ vpc.delete(p)
1675
+ end
1676
+ }
1677
+ ['cloud', 'id', 'name', 'deploy_id', 'habitat', 'credentials'].each { |field|
1678
+ realvpc[field] = vpc[field] if !vpc[field].nil?
1679
+ }
1680
+ if !realvpc['id'].nil? and !realvpc['id'].empty?
1681
+ # Stupid kludge for Google cloud_ids which are sometimes URLs and
1682
+ # sometimes not. Requirements are inconsistent from scenario to
1683
+ # scenario.
1684
+ name = name + "-" + realvpc['id'].gsub(/.*\//, "")
1685
+ realvpc['id'] = getTail("id", value: realvpc['id'], prettyname: "Admin Firewall Ruleset #{name} Target VPC", cloudtype: "AWS::EC2::VPC::Id") if realvpc["id"].is_a?(String)
1686
+ elsif !realvpc['name'].nil?
1687
+ name = name + "-" + realvpc['name']
1688
+ end
1689
+ end
1690
+
1691
+
1299
1692
  acl = {"name" => name, "rules" => rules, "vpc" => realvpc, "cloud" => cloud, "admin" => true, "credentials" => credentials }
1300
1693
  acl.delete("vpc") if !acl["vpc"]
1301
- acl["region"] = region if !region.nil? and !region.empty?
1694
+ if !resclass.isGlobal? and !region.nil? and !region.empty?
1695
+ acl["region"] = region
1696
+ end
1302
1697
  @admin_firewall_rules << acl if !@admin_firewall_rules.include?(acl)
1303
1698
  return {"type" => "firewall_rule", "name" => name}
1304
1699
  end
@@ -1476,33 +1871,57 @@ module MU
1476
1871
  binding
1477
1872
  end
1478
1873
 
1479
- def self.set_defaults(conf_chunk = config, schema_chunk = schema, depth = 0, siblings = nil)
1874
+ def applySchemaDefaults(conf_chunk = config, schema_chunk = schema, depth = 0, siblings = nil, type: nil)
1480
1875
  return if schema_chunk.nil?
1481
1876
 
1482
1877
  if conf_chunk != nil and schema_chunk["properties"].kind_of?(Hash) and conf_chunk.is_a?(Hash)
1878
+
1483
1879
  if schema_chunk["properties"]["creation_style"].nil? or
1484
1880
  schema_chunk["properties"]["creation_style"] != "existing"
1485
1881
  schema_chunk["properties"].each_pair { |key, subschema|
1486
- new_val = self.set_defaults(conf_chunk[key], subschema, depth+1, conf_chunk)
1487
- conf_chunk[key] = new_val if new_val != nil
1882
+ shortclass = if conf_chunk[key]
1883
+ shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(key)
1884
+ shortclass
1885
+ else
1886
+ nil
1887
+ end
1888
+
1889
+ new_val = applySchemaDefaults(conf_chunk[key], subschema, depth+1, conf_chunk, type: shortclass).dup
1890
+
1891
+ conf_chunk[key] = Marshal.load(Marshal.dump(new_val)) if !new_val.nil?
1488
1892
  }
1489
1893
  end
1490
1894
  elsif schema_chunk["type"] == "array" and conf_chunk.kind_of?(Array)
1491
1895
  conf_chunk.map! { |item|
1492
- self.set_defaults(item, schema_chunk["items"], depth+1, conf_chunk)
1896
+ # If we're working on a resource type, go get implementation-specific
1897
+ # schema information so that we set those defaults correctly.
1898
+ realschema = if type and schema_chunk["items"] and schema_chunk["items"]["properties"] and item["cloud"]
1899
+
1900
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(item["cloud"]).const_get(type)
1901
+ toplevel_required, cloudschema = cloudclass.schema(self)
1902
+
1903
+ newschema = schema_chunk["items"].dup
1904
+ newschema["properties"].merge!(cloudschema)
1905
+ newschema
1906
+ else
1907
+ schema_chunk["items"].dup
1908
+ end
1909
+
1910
+ applySchemaDefaults(item, realschema, depth+1, conf_chunk, type: type).dup
1493
1911
  }
1494
1912
  else
1495
1913
  if conf_chunk.nil? and !schema_chunk["default_if"].nil? and !siblings.nil?
1496
1914
  schema_chunk["default_if"].each { |cond|
1497
1915
  if siblings[cond["key_is"]] == cond["value_is"]
1498
- return cond["set"]
1916
+ return Marshal.load(Marshal.dump(cond["set"]))
1499
1917
  end
1500
1918
  }
1501
1919
  end
1502
1920
  if conf_chunk.nil? and schema_chunk["default"] != nil
1503
- return schema_chunk["default"].dup
1921
+ return Marshal.load(Marshal.dump(schema_chunk["default"]))
1504
1922
  end
1505
1923
  end
1924
+
1506
1925
  return conf_chunk
1507
1926
  end
1508
1927
 
@@ -1512,48 +1931,46 @@ module MU
1512
1931
  def self.check_dependencies(config)
1513
1932
  ok = true
1514
1933
 
1515
- config.each { |type|
1516
- if type.instance_of?(Array)
1517
- type.each { |container|
1518
- if container.instance_of?(Array)
1519
- container.each { |resource|
1520
- if resource.kind_of?(Hash) and resource["dependencies"] != nil
1521
- append = []
1522
- delete = []
1523
- resource["dependencies"].each { |dependency|
1524
- collection = dependency["type"]+"s"
1525
- found = false
1526
- names_seen = []
1527
- if config[collection] != nil
1528
- config[collection].each { |service|
1529
- names_seen << service["name"].to_s
1530
- found = true if service["name"].to_s == dependency["name"].to_s
1531
- if service["virtual_name"]
1532
- names_seen << service["virtual_name"].to_s
1533
- found = true if service["virtual_name"].to_s == dependency["name"].to_s
1534
- append_me = dependency.dup
1535
- append_me['name'] = service['name']
1536
- append << append_me
1537
- delete << dependency
1538
- end
1539
- }
1540
- end
1541
- if !found
1542
- MU.log "Missing dependency: #{type[0]}{#{resource['name']}} needs #{collection}{#{dependency['name']}}", MU::ERR, details: names_seen
1543
- ok = false
1934
+ config.each_pair { |type, values|
1935
+ if values.instance_of?(Array)
1936
+ values.each { |resource|
1937
+ if resource.kind_of?(Hash) and !resource["dependencies"].nil?
1938
+ append = []
1939
+ delete = []
1940
+ resource["dependencies"].each { |dependency|
1941
+ shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(dependency["type"])
1942
+ found = false
1943
+ names_seen = []
1944
+ if !config[cfg_plural].nil?
1945
+ config[cfg_plural].each { |service|
1946
+ names_seen << service["name"].to_s
1947
+ found = true if service["name"].to_s == dependency["name"].to_s
1948
+ if service["virtual_name"]
1949
+ names_seen << service["virtual_name"].to_s
1950
+ if service["virtual_name"].to_s == dependency["name"].to_s
1951
+ found = true
1952
+ append_me = dependency.dup
1953
+ append_me['name'] = service['name']
1954
+ append << append_me
1955
+ delete << dependency
1956
+ end
1544
1957
  end
1545
1958
  }
1546
- if append.size > 0
1547
- append.uniq!
1548
- resource["dependencies"].concat(append)
1549
- end
1550
- if delete.size > 0
1551
- delete.each { |delete_me|
1552
- resource["dependencies"].delete(delete_me)
1553
- }
1554
- end
1959
+ end
1960
+ if !found
1961
+ MU.log "Missing dependency: #{type}{#{resource['name']}} needs #{cfg_name}{#{dependency['name']}}", MU::ERR, details: names_seen
1962
+ ok = false
1555
1963
  end
1556
1964
  }
1965
+ if append.size > 0
1966
+ append.uniq!
1967
+ resource["dependencies"].concat(append)
1968
+ end
1969
+ if delete.size > 0
1970
+ delete.each { |delete_me|
1971
+ resource["dependencies"].delete(delete_me)
1972
+ }
1973
+ end
1557
1974
  end
1558
1975
  }
1559
1976
  end
@@ -1622,23 +2039,39 @@ module MU
1622
2039
 
1623
2040
 
1624
2041
  # Given a bare hash describing a resource, insert default values which can
1625
- # be inherited from the current live parent configuration.
2042
+ # be inherited from its parent or from the root of the BoK.
1626
2043
  # @param kitten [Hash]: A resource descriptor
1627
2044
  # @param type [String]: The type of resource this is ("servers" etc)
1628
- def inheritDefaults(kitten, type)
2045
+ def applyInheritedDefaults(kitten, type)
2046
+ kitten['cloud'] ||= @config['cloud']
1629
2047
  kitten['cloud'] ||= MU::Config.defaultCloud
2048
+
1630
2049
  cloudclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud'])
1631
2050
  shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
1632
2051
  resclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud']).const_get(shortclass)
1633
2052
 
1634
- schema_fields = ["us_only", "scrub_mu_isms", "credentials"]
2053
+ schema_fields = ["us_only", "scrub_mu_isms", "credentials", "billing_acct"]
1635
2054
  if !resclass.isGlobal?
2055
+ kitten['region'] ||= @config['region']
2056
+ kitten['region'] ||= cloudclass.myRegion(kitten['credentials'])
1636
2057
  schema_fields << "region"
1637
2058
  end
1638
2059
 
2060
+ kitten['credentials'] ||= @config['credentials']
2061
+ kitten['credentials'] ||= cloudclass.credConfig(name_only: true)
2062
+
2063
+ kitten['us_only'] ||= @config['us_only']
2064
+ kitten['us_only'] ||= false
2065
+
2066
+ kitten['scrub_mu_isms'] ||= @config['scrub_mu_isms']
2067
+ kitten['scrub_mu_isms'] ||= false
2068
+
1639
2069
  if kitten['cloud'] == "Google"
1640
- kitten["project"] ||= MU::Cloud::Google.defaultProject(kitten['credentials'])
1641
- schema_fields << "project"
2070
+ # TODO this should be cloud-generic (handle AWS accounts, Azure subscriptions)
2071
+ if resclass.canLiveIn.include?(:Habitat)
2072
+ kitten["project"] ||= MU::Cloud::Google.defaultProject(kitten['credentials'])
2073
+ schema_fields << "project"
2074
+ end
1642
2075
  if kitten['region'].nil? and !kitten['#MU_CLOUDCLASS'].nil? and
1643
2076
  !resclass.isGlobal? and
1644
2077
  ![MU::Cloud::VPC, MU::Cloud::FirewallRule].include?(kitten['#MU_CLOUDCLASS'])
@@ -1647,21 +2080,15 @@ module MU
1647
2080
  end
1648
2081
  kitten['region'] ||= MU::Cloud::Google.myRegion
1649
2082
  end
1650
- elsif kitten["cloud"] == "AWS" and !resclass.isGlobal?
2083
+ elsif kitten["cloud"] == "AWS" and !resclass.isGlobal? and !kitten['region']
1651
2084
  if MU::Cloud::AWS.myRegion.nil?
1652
2085
  raise ValidationError, "AWS resource declared without a region, but no default AWS region found"
1653
2086
  end
1654
2087
  kitten['region'] ||= MU::Cloud::AWS.myRegion
1655
2088
  end
1656
2089
 
1657
- kitten['us_only'] ||= @config['us_only']
1658
- kitten['us_only'] ||= false
1659
2090
 
1660
- kitten['scrub_mu_isms'] ||= @config['scrub_mu_isms']
1661
- kitten['scrub_mu_isms'] ||= false
1662
-
1663
- kitten['credentials'] ||= @config['credentials']
1664
- kitten['credentials'] ||= cloudclass.credConfig(name_only: true)
2091
+ kitten['billing_acct'] ||= @config['billing_acct'] if @config['billing_acct']
1665
2092
 
1666
2093
  kitten["dependencies"] ||= []
1667
2094
 
@@ -1677,7 +2104,6 @@ module MU
1677
2104
 
1678
2105
  def validate(config = @config)
1679
2106
  ok = true
1680
- plain_cfg = MU::Config.manxify(Marshal.load(Marshal.dump(config)))
1681
2107
 
1682
2108
  count = 0
1683
2109
  @kittens ||= {}
@@ -1687,7 +2113,7 @@ module MU
1687
2113
  @kittens[type] = config[type]
1688
2114
  @kittens[type] ||= []
1689
2115
  @kittens[type].each { |k|
1690
- inheritDefaults(k, type)
2116
+ applyInheritedDefaults(k, type)
1691
2117
  }
1692
2118
  count = count + @kittens[type].size
1693
2119
  }
@@ -1708,6 +2134,12 @@ module MU
1708
2134
  acl = resolveIntraStackFirewallRefs(acl)
1709
2135
  }
1710
2136
 
2137
+ # VPCs do complex things in their cloud-layer validation that other
2138
+ # resources tend to need, like subnet allocation, so hit them early.
2139
+ @kittens["vpcs"].each { |vpc|
2140
+ ok = false if !insertKitten(vpc, "vpcs")
2141
+ }
2142
+
1711
2143
  # Make sure validation has been called for all on-the-fly generated
1712
2144
  # resources.
1713
2145
  validated_something_new = false
@@ -1715,9 +2147,10 @@ module MU
1715
2147
  validated_something_new = false
1716
2148
  types.each { |type|
1717
2149
  @kittens[type].each { |descriptor|
1718
- if !descriptor["#MU_VALIDATED"]
2150
+ if !descriptor["#MU_VALIDATION_ATTEMPTED"]
1719
2151
  validated_something_new = true
1720
2152
  ok = false if !insertKitten(descriptor, type)
2153
+ descriptor["#MU_VALIDATION_ATTEMPTED"] = true
1721
2154
  end
1722
2155
  }
1723
2156
  }
@@ -1918,7 +2351,6 @@ module MU
1918
2351
  return docstring
1919
2352
  end
1920
2353
 
1921
- return nil
1922
2354
  end
1923
2355
 
1924
2356
  def self.dependencies_primitive
@@ -1969,6 +2401,57 @@ module MU
1969
2401
  end
1970
2402
  end
1971
2403
 
2404
+ # Load and validate the schema for an individual resource class, optionally
2405
+ # merging cloud-specific schema components.
2406
+ # @param type [String]: The resource type to load
2407
+ # @param cloud [String]: A specific cloud, whose implementation's schema of this resource we will merge
2408
+ # @return [Hash]
2409
+ def self.loadResourceSchema(type, cloud: nil)
2410
+ valid = true
2411
+ shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
2412
+ schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
2413
+
2414
+ [:schema, :validate].each { |method|
2415
+ if !schemaclass.respond_to?(method)
2416
+ MU.log "MU::Config::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
2417
+ return [nil, false] if method == :schema
2418
+ valid = false
2419
+ end
2420
+ }
2421
+
2422
+ schema = schemaclass.schema.dup
2423
+
2424
+ schema["properties"]["virtual_name"] = {
2425
+ "description" => "Internal use.",
2426
+ "type" => "string"
2427
+ }
2428
+ schema["properties"]["dependencies"] = MU::Config.dependencies_primitive
2429
+ schema["properties"]["cloud"] = MU::Config.cloud_primitive
2430
+ schema["properties"]["credentials"] = MU::Config.credentials_primitive
2431
+ schema["title"] = type.to_s
2432
+
2433
+ if cloud
2434
+ cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get(shortclass)
2435
+
2436
+ if cloudclass.respond_to?(:schema)
2437
+ reqd, cloudschema = cloudclass.schema
2438
+ cloudschema.each { |key, cfg|
2439
+ if schema["properties"][key]
2440
+ schemaMerge(schema["properties"][key], cfg, cloud)
2441
+ else
2442
+ schema["properties"][key] = cfg.dup
2443
+ end
2444
+ }
2445
+ else
2446
+ MU.log "MU::Cloud::#{cloud}::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
2447
+ valid = false
2448
+ end
2449
+
2450
+ end
2451
+
2452
+ return [schema, valid]
2453
+ end
2454
+
1972
2455
  @@schema = {
1973
2456
  "$schema" => "http://json-schema.org/draft-04/schema#",
1974
2457
  "title" => "MU Application",
@@ -1986,7 +2469,11 @@ module MU
1986
2469
  },
1987
2470
  "project" => {
1988
2471
  "type" => "string",
1989
- "description" => "GOOGLE: The project into which to deploy resources"
2472
+ "description" => "**GOOGLE ONLY**: The project into which to deploy resources"
2473
+ },
2474
+ "billing_acct" => {
2475
+ "type" => "string",
2476
+ "description" => "**GOOGLE ONLY**: Billing account ID to associate with a newly-created Google Project. If not specified, will attempt to locate a billing account associated with the default project for our credentials.",
1990
2477
  },
1991
2478
  "region" => MU::Config.region_primitive,
1992
2479
  "credentials" => MU::Config.credentials_primitive,
@@ -2084,28 +2571,16 @@ module MU
2084
2571
  end
2085
2572
  }
2086
2573
 
2574
+
2087
2575
  MU::Cloud.resource_types.each_pair { |type, cfg|
2088
2576
  begin
2089
- schemaclass = Object.const_get("MU").const_get("Config").const_get(type)
2090
- [:schema, :validate].each { |method|
2091
- if !schemaclass.respond_to?(method)
2092
- MU.log "MU::Config::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
2093
- failed << type
2094
- end
2095
- }
2577
+ schema, valid = loadResourceSchema(type)
2578
+ failed << type if !valid
2096
2579
  next if failed.include?(type)
2097
2580
  @@schema["properties"][cfg[:cfg_plural]] = {
2098
2581
  "type" => "array",
2099
- "items" => schemaclass.schema
2100
- }
2101
- @@schema["properties"][cfg[:cfg_plural]]["items"]["properties"]["virtual_name"] = {
2102
- "description" => "Internal use.",
2103
- "type" => "string"
2582
+ "items" => schema
2104
2583
  }
2105
- @@schema["properties"][cfg[:cfg_plural]]["items"]["properties"]["dependencies"] = MU::Config.dependencies_primitive
2106
- @@schema["properties"][cfg[:cfg_plural]]["items"]["properties"]["cloud"] = MU::Config.cloud_primitive
2107
- @@schema["properties"][cfg[:cfg_plural]]["items"]["properties"]["credentials"] = MU::Config.credentials_primitive
2108
- @@schema["properties"][cfg[:cfg_plural]]["items"]["title"] = type.to_s
2109
2584
  rescue NameError => e
2110
2585
  failed << type
2111
2586
  MU.log "Error loading #{type} schema from mu/config/#{cfg[:cfg_name]}", MU::ERR, details: "\t"+e.inspect+"\n\t"+e.backtrace[0]