cloud-mu 2.1.0beta → 3.0.0beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -28,6 +28,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]