cloud-mu 3.1.3 → 3.3.0

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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +15 -3
  3. data/ansible/roles/mu-windows/README.md +33 -0
  4. data/ansible/roles/mu-windows/defaults/main.yml +2 -0
  5. data/ansible/roles/mu-windows/files/LaunchConfig.json +9 -0
  6. data/ansible/roles/mu-windows/files/config.xml +76 -0
  7. data/ansible/roles/mu-windows/handlers/main.yml +2 -0
  8. data/ansible/roles/mu-windows/meta/main.yml +53 -0
  9. data/ansible/roles/mu-windows/tasks/main.yml +36 -0
  10. data/ansible/roles/mu-windows/tests/inventory +2 -0
  11. data/ansible/roles/mu-windows/tests/test.yml +5 -0
  12. data/ansible/roles/mu-windows/vars/main.yml +2 -0
  13. data/bin/mu-adopt +21 -13
  14. data/bin/mu-azure-tests +57 -0
  15. data/bin/mu-cleanup +2 -4
  16. data/bin/mu-configure +52 -0
  17. data/bin/mu-deploy +3 -3
  18. data/bin/mu-findstray-tests +25 -0
  19. data/bin/mu-gen-docs +2 -4
  20. data/bin/mu-load-config.rb +4 -4
  21. data/bin/mu-node-manage +15 -16
  22. data/bin/mu-run-tests +147 -37
  23. data/cloud-mu.gemspec +22 -20
  24. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  25. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  26. data/cookbooks/mu-tools/libraries/helper.rb +3 -2
  27. data/cookbooks/mu-tools/libraries/monkey.rb +35 -0
  28. data/cookbooks/mu-tools/recipes/apply_security.rb +14 -14
  29. data/cookbooks/mu-tools/recipes/aws_api.rb +9 -0
  30. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  31. data/cookbooks/mu-tools/recipes/google_api.rb +2 -2
  32. data/cookbooks/mu-tools/recipes/selinux.rb +2 -1
  33. data/cookbooks/mu-tools/recipes/windows-client.rb +163 -164
  34. data/cookbooks/mu-tools/resources/disk.rb +1 -1
  35. data/cookbooks/mu-tools/resources/windows_users.rb +44 -43
  36. data/extras/clean-stock-amis +25 -19
  37. data/extras/generate-stock-images +1 -0
  38. data/extras/image-generators/AWS/win2k12.yaml +18 -13
  39. data/extras/image-generators/AWS/win2k16.yaml +18 -13
  40. data/extras/image-generators/AWS/win2k19.yaml +21 -0
  41. data/extras/image-generators/Google/centos6.yaml +1 -0
  42. data/extras/image-generators/Google/centos7.yaml +1 -1
  43. data/modules/mommacat.ru +6 -16
  44. data/modules/mu.rb +158 -111
  45. data/modules/mu/adoption.rb +404 -71
  46. data/modules/mu/cleanup.rb +221 -306
  47. data/modules/mu/cloud.rb +129 -1633
  48. data/modules/mu/cloud/database.rb +49 -0
  49. data/modules/mu/cloud/dnszone.rb +44 -0
  50. data/modules/mu/cloud/machine_images.rb +212 -0
  51. data/modules/mu/cloud/providers.rb +81 -0
  52. data/modules/mu/cloud/resource_base.rb +926 -0
  53. data/modules/mu/cloud/server.rb +40 -0
  54. data/modules/mu/cloud/server_pool.rb +1 -0
  55. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  56. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  57. data/modules/mu/cloud/wrappers.rb +169 -0
  58. data/modules/mu/config.rb +171 -1767
  59. data/modules/mu/config/alarm.rb +2 -6
  60. data/modules/mu/config/bucket.rb +32 -3
  61. data/modules/mu/config/cache_cluster.rb +2 -2
  62. data/modules/mu/config/cdn.rb +100 -0
  63. data/modules/mu/config/collection.rb +4 -4
  64. data/modules/mu/config/container_cluster.rb +9 -4
  65. data/modules/mu/config/database.rb +84 -105
  66. data/modules/mu/config/database.yml +1 -2
  67. data/modules/mu/config/dnszone.rb +10 -9
  68. data/modules/mu/config/doc_helpers.rb +516 -0
  69. data/modules/mu/config/endpoint.rb +5 -4
  70. data/modules/mu/config/firewall_rule.rb +103 -4
  71. data/modules/mu/config/folder.rb +4 -4
  72. data/modules/mu/config/function.rb +19 -10
  73. data/modules/mu/config/group.rb +4 -4
  74. data/modules/mu/config/habitat.rb +4 -4
  75. data/modules/mu/config/job.rb +89 -0
  76. data/modules/mu/config/loadbalancer.rb +60 -14
  77. data/modules/mu/config/log.rb +4 -4
  78. data/modules/mu/config/msg_queue.rb +4 -4
  79. data/modules/mu/config/nosqldb.rb +4 -4
  80. data/modules/mu/config/notifier.rb +10 -21
  81. data/modules/mu/config/ref.rb +411 -0
  82. data/modules/mu/config/role.rb +4 -4
  83. data/modules/mu/config/schema_helpers.rb +509 -0
  84. data/modules/mu/config/search_domain.rb +4 -4
  85. data/modules/mu/config/server.rb +98 -71
  86. data/modules/mu/config/server.yml +1 -0
  87. data/modules/mu/config/server_pool.rb +5 -9
  88. data/modules/mu/config/storage_pool.rb +1 -1
  89. data/modules/mu/config/tail.rb +200 -0
  90. data/modules/mu/config/user.rb +4 -4
  91. data/modules/mu/config/vpc.rb +71 -27
  92. data/modules/mu/config/vpc.yml +0 -1
  93. data/modules/mu/defaults/AWS.yaml +91 -68
  94. data/modules/mu/defaults/Azure.yaml +1 -0
  95. data/modules/mu/defaults/Google.yaml +3 -2
  96. data/modules/mu/deploy.rb +43 -26
  97. data/modules/mu/groomer.rb +17 -2
  98. data/modules/mu/groomers/ansible.rb +188 -41
  99. data/modules/mu/groomers/chef.rb +116 -55
  100. data/modules/mu/logger.rb +127 -148
  101. data/modules/mu/master.rb +410 -2
  102. data/modules/mu/master/chef.rb +3 -4
  103. data/modules/mu/master/ldap.rb +3 -3
  104. data/modules/mu/master/ssl.rb +12 -3
  105. data/modules/mu/mommacat.rb +218 -2612
  106. data/modules/mu/mommacat/daemon.rb +403 -0
  107. data/modules/mu/mommacat/naming.rb +473 -0
  108. data/modules/mu/mommacat/search.rb +495 -0
  109. data/modules/mu/mommacat/storage.rb +722 -0
  110. data/modules/mu/{clouds → providers}/README.md +1 -1
  111. data/modules/mu/{clouds → providers}/aws.rb +380 -122
  112. data/modules/mu/{clouds → providers}/aws/alarm.rb +7 -5
  113. data/modules/mu/{clouds → providers}/aws/bucket.rb +297 -59
  114. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +37 -71
  115. data/modules/mu/providers/aws/cdn.rb +782 -0
  116. data/modules/mu/{clouds → providers}/aws/collection.rb +26 -25
  117. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +724 -744
  118. data/modules/mu/providers/aws/database.rb +1744 -0
  119. data/modules/mu/{clouds → providers}/aws/dnszone.rb +88 -70
  120. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  121. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +220 -247
  122. data/modules/mu/{clouds → providers}/aws/folder.rb +8 -8
  123. data/modules/mu/{clouds → providers}/aws/function.rb +300 -142
  124. data/modules/mu/{clouds → providers}/aws/group.rb +31 -29
  125. data/modules/mu/{clouds → providers}/aws/habitat.rb +18 -15
  126. data/modules/mu/providers/aws/job.rb +466 -0
  127. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +66 -56
  128. data/modules/mu/{clouds → providers}/aws/log.rb +17 -14
  129. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +29 -19
  130. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +114 -16
  131. data/modules/mu/{clouds → providers}/aws/notifier.rb +142 -65
  132. data/modules/mu/{clouds → providers}/aws/role.rb +158 -118
  133. data/modules/mu/{clouds → providers}/aws/search_domain.rb +201 -59
  134. data/modules/mu/{clouds → providers}/aws/server.rb +844 -1139
  135. data/modules/mu/{clouds → providers}/aws/server_pool.rb +74 -65
  136. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +26 -44
  137. data/modules/mu/{clouds → providers}/aws/user.rb +24 -25
  138. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  139. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  140. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +2 -1
  141. data/modules/mu/{clouds → providers}/aws/vpc.rb +525 -931
  142. data/modules/mu/providers/aws/vpc_subnet.rb +286 -0
  143. data/modules/mu/{clouds → providers}/azure.rb +29 -9
  144. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +3 -8
  145. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +18 -11
  146. data/modules/mu/{clouds → providers}/azure/habitat.rb +8 -6
  147. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +5 -5
  148. data/modules/mu/{clouds → providers}/azure/role.rb +8 -10
  149. data/modules/mu/{clouds → providers}/azure/server.rb +97 -49
  150. data/modules/mu/{clouds → providers}/azure/user.rb +6 -8
  151. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  152. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  153. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  154. data/modules/mu/{clouds → providers}/azure/vpc.rb +16 -21
  155. data/modules/mu/{clouds → providers}/cloudformation.rb +18 -7
  156. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  157. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  158. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  159. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  160. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  161. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  162. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  163. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  164. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  165. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  166. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +5 -7
  167. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  168. data/modules/mu/{clouds → providers}/google.rb +68 -30
  169. data/modules/mu/{clouds → providers}/google/bucket.rb +13 -15
  170. data/modules/mu/{clouds → providers}/google/container_cluster.rb +85 -78
  171. data/modules/mu/{clouds → providers}/google/database.rb +11 -21
  172. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +15 -14
  173. data/modules/mu/{clouds → providers}/google/folder.rb +20 -17
  174. data/modules/mu/{clouds → providers}/google/function.rb +140 -168
  175. data/modules/mu/{clouds → providers}/google/group.rb +29 -34
  176. data/modules/mu/{clouds → providers}/google/habitat.rb +21 -22
  177. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +19 -21
  178. data/modules/mu/{clouds → providers}/google/role.rb +94 -58
  179. data/modules/mu/{clouds → providers}/google/server.rb +243 -156
  180. data/modules/mu/{clouds → providers}/google/server_pool.rb +26 -45
  181. data/modules/mu/{clouds → providers}/google/user.rb +95 -31
  182. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  183. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  184. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  185. data/modules/mu/{clouds → providers}/google/vpc.rb +103 -79
  186. data/modules/tests/aws-jobs-functions.yaml +46 -0
  187. data/modules/tests/bucket.yml +4 -0
  188. data/modules/tests/centos6.yaml +15 -0
  189. data/modules/tests/centos7.yaml +15 -0
  190. data/modules/tests/centos8.yaml +12 -0
  191. data/modules/tests/ecs.yaml +23 -0
  192. data/modules/tests/eks.yaml +1 -1
  193. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  194. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  195. data/modules/tests/includes-and-params.yaml +2 -1
  196. data/modules/tests/microservice_app.yaml +288 -0
  197. data/modules/tests/rds.yaml +108 -0
  198. data/modules/tests/regrooms/aws-iam.yaml +201 -0
  199. data/modules/tests/regrooms/bucket.yml +19 -0
  200. data/modules/tests/regrooms/rds.yaml +123 -0
  201. data/modules/tests/server-with-scrub-muisms.yaml +2 -1
  202. data/modules/tests/super_complex_bok.yml +2 -2
  203. data/modules/tests/super_simple_bok.yml +3 -5
  204. data/modules/tests/win2k12.yaml +17 -5
  205. data/modules/tests/win2k16.yaml +25 -0
  206. data/modules/tests/win2k19.yaml +25 -0
  207. data/requirements.txt +1 -0
  208. data/spec/mu/clouds/azure_spec.rb +2 -2
  209. metadata +240 -154
  210. data/extras/image-generators/AWS/windows.yaml +0 -18
  211. data/modules/mu/clouds/aws/database.rb +0 -1985
  212. data/modules/mu/clouds/aws/endpoint.rb +0 -592
@@ -0,0 +1,169 @@
1
+ # Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+ # Plugins under this namespace serve as interfaces to cloud providers and
17
+ # other provisioning layers.
18
+ class Cloud
19
+
20
+ # In this file: generic class method wrappers for all resource types.
21
+
22
+ @@resource_types.keys.each { |name|
23
+ Object.const_get("MU").const_get("Cloud").const_get(name).class_eval {
24
+
25
+ def self.shortname
26
+ name.sub(/.*?::([^:]+)$/, '\1')
27
+ end
28
+
29
+ def self.cfg_plural
30
+ MU::Cloud.resource_types[shortname.to_sym][:cfg_plural]
31
+ end
32
+
33
+ def self.has_multiples
34
+ MU::Cloud.resource_types[shortname.to_sym][:has_multiples]
35
+ end
36
+
37
+ def self.cfg_name
38
+ MU::Cloud.resource_types[shortname.to_sym][:cfg_name]
39
+ end
40
+
41
+ def self.can_live_in_vpc
42
+ MU::Cloud.resource_types[shortname.to_sym][:can_live_in_vpc]
43
+ end
44
+
45
+ def self.waits_on_parent_completion
46
+ MU::Cloud.resource_types[shortname.to_sym][:waits_on_parent_completion]
47
+ end
48
+
49
+ def self.deps_wait_on_my_creation
50
+ MU::Cloud.resource_types[shortname.to_sym][:deps_wait_on_my_creation]
51
+ end
52
+
53
+ # Defaults any resources that don't declare their release-readiness to
54
+ # ALPHA. That'll learn 'em.
55
+ def self.quality
56
+ MU::Cloud::ALPHA
57
+ end
58
+
59
+ # Return a list of "container" artifacts, by class, that apply to this
60
+ # resource type in a cloud provider. This is so methods that call find
61
+ # know whether to call +find+ with identifiers for parent resources.
62
+ # This is similar in purpose to the +isGlobal?+ resource class method,
63
+ # which tells our search functions whether or not a resource scopes to
64
+ # a region. In almost all cases this is one-entry list consisting of
65
+ # +:Habitat+. Notable exceptions include most implementations of
66
+ # +Habitat+, which either reside inside a +:Folder+ or nothing at all;
67
+ # whereas a +:Folder+ tends to not have any containing parent. Very few
68
+ # resource implementations will need to override this.
69
+ # A +nil+ entry in this list is interpreted as "this resource can be
70
+ # global."
71
+ # @return [Array<Symbol,nil>]
72
+ def self.canLiveIn
73
+ if self.shortname == "Folder"
74
+ [nil, :Folder]
75
+ elsif self.shortname == "Habitat"
76
+ [:Folder]
77
+ else
78
+ [:Habitat]
79
+ end
80
+ end
81
+
82
+ def self.find(*flags)
83
+ allfound = {}
84
+
85
+ MU::Cloud.availableClouds.each { |cloud|
86
+ begin
87
+ args = flags.first
88
+ next if args[:cloud] and args[:cloud] != cloud
89
+ # skip this cloud if we have a region argument that makes no
90
+ # sense there
91
+ cloudbase = MU::Cloud.cloudClass(cloud)
92
+ next if cloudbase.listCredentials.nil? or cloudbase.listCredentials.empty? or cloudbase.credConfig(args[:credentials]).nil?
93
+ if args[:region] and cloudbase.respond_to?(:listRegions)
94
+ if !cloudbase.listRegions(credentials: args[:credentials])
95
+ MU.log "Failed to get region list for credentials #{args[:credentials]} in cloud #{cloud}", MU::ERR, details: caller
96
+ else
97
+ next if !cloudbase.listRegions(credentials: args[:credentials]).include?(args[:region])
98
+ end
99
+ end
100
+ begin
101
+ cloudclass = MU::Cloud.resourceClass(cloud, shortname)
102
+ rescue MU::MuError
103
+ next
104
+ end
105
+
106
+ found = cloudclass.find(args)
107
+ if !found.nil?
108
+ if found.is_a?(Hash)
109
+ allfound.merge!(found)
110
+ else
111
+ raise MuError, "#{cloudclass}.find returned a non-Hash result"
112
+ end
113
+ end
114
+ rescue MuCloudResourceNotImplemented
115
+ end
116
+ }
117
+ allfound
118
+ end
119
+
120
+ # Wrapper for the cleanup class method of underlying cloud object implementations.
121
+ def self.cleanup(*flags)
122
+ ok = true
123
+ params = flags.first
124
+ clouds = MU::Cloud.supportedClouds
125
+ if params[:cloud]
126
+ clouds = [params[:cloud]]
127
+ params.delete(:cloud)
128
+ end
129
+ params[:deploy_id] ||= MU.deploy_id
130
+ if !params[:deploy_id] or params[:deploy_id].empty?
131
+ raise MuError, "Can't call cleanup methods without a deploy id"
132
+ end
133
+
134
+ clouds.each { |cloud|
135
+ begin
136
+ cloudclass = MU::Cloud.resourceClass(cloud, shortname)
137
+
138
+ if cloudclass.isGlobal?
139
+ params.delete(:region)
140
+ end
141
+
142
+ raise MuCloudResourceNotImplemented if !cloudclass.respond_to?(:cleanup) or cloudclass.method(:cleanup).owner.to_s != "#<Class:#{cloudclass}>"
143
+ MU.log "Invoking #{cloudclass}.cleanup from #{shortname}", MU::DEBUG, details: flags
144
+ cloudclass.cleanup(params)
145
+ rescue MuCloudResourceNotImplemented
146
+ MU.log "No #{cloud} implementation of #{shortname}.cleanup, skipping", MU::DEBUG, details: flags
147
+ rescue StandardError => e
148
+ in_msg = cloud
149
+ if params and params[:region]
150
+ in_msg += " "+params[:region]
151
+ end
152
+ if params and params[:flags] and params[:flags]["project"] and !params[:flags]["project"].empty?
153
+ in_msg += " project "+params[:flags]["project"]
154
+ end
155
+ MU.log "Skipping #{shortname} cleanup method in #{in_msg} due to #{e.class.name}: #{e.message}", MU::WARN, details: e.backtrace
156
+ ok = false
157
+ end
158
+ }
159
+ MU::MommaCat.unlockAll
160
+
161
+ ok
162
+ end
163
+
164
+ } # end dynamic class generation block
165
+ } # end resource type iteration
166
+
167
+ end
168
+
169
+ end
@@ -18,6 +18,10 @@ require 'erb'
18
18
  require 'pp'
19
19
  require 'json-schema'
20
20
  require 'net/http'
21
+ require 'mu/config/schema_helpers'
22
+ require 'mu/config/tail'
23
+ require 'mu/config/ref'
24
+ require 'mu/config/doc_helpers'
21
25
  autoload :GraphViz, 'graphviz'
22
26
  autoload :ChronicDuration, 'chronic_duration'
23
27
 
@@ -35,35 +39,6 @@ module MU
35
39
  class DeployParamError < MuError
36
40
  end
37
41
 
38
- # The default cloud provider for new resources. Must exist in MU.supportedClouds
39
- # return [String]
40
- def self.defaultCloud
41
- configured = {}
42
- MU::Cloud.supportedClouds.each { |cloud|
43
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
44
- if $MU_CFG[cloud.downcase] and !$MU_CFG[cloud.downcase].empty?
45
- configured[cloud] = $MU_CFG[cloud.downcase].size
46
- configured[cloud] += 0.5 if cloudclass.hosted? # tiebreaker
47
- end
48
- }
49
- if configured.size > 0
50
- return configured.keys.sort { |a, b|
51
- configured[b] <=> configured[a]
52
- }.first
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
- }
58
- return MU::Cloud.supportedClouds.first
59
- end
60
- end
61
-
62
- # The default grooming agent for new resources. Must exist in MU.supportedGroomers.
63
- def self.defaultGroomer
64
- MU.localOnly ? "Ansible" : "Chef"
65
- end
66
-
67
42
  attr_accessor :nat_routes
68
43
  attr_reader :skipinitialupdates
69
44
 
@@ -75,120 +50,6 @@ module MU
75
50
  @@config_path
76
51
  end
77
52
 
78
- # Accessor for our Basket of Kittens schema definition
79
- def self.schema
80
- @@schema
81
- end
82
-
83
- # Deep merge a configuration hash so we can meld different cloud providers'
84
- # schemas together, while preserving documentation differences
85
- def self.schemaMerge(orig, new, cloud)
86
- if new.is_a?(Hash)
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
91
- if orig and orig.has_key?(k)
92
- elsif orig
93
- orig[k] = new[k]
94
- else
95
- orig = new
96
- end
97
- schemaMerge(orig[k], new[k], cloud)
98
- }
99
- elsif orig.is_a?(Array) and new
100
- orig.concat(new)
101
- orig.uniq!
102
- elsif new.is_a?(String)
103
- orig ||= ""
104
- orig += "\n" if !orig.empty?
105
- orig += "+#{cloud.upcase}+: "+new
106
- else
107
- # XXX I think this is a NOOP?
108
- end
109
- end
110
-
111
- # Accessor for our Basket of Kittens schema definition, with various
112
- # cloud-specific details merged so we can generate documentation for them.
113
- def self.docSchema
114
- docschema = Marshal.load(Marshal.dump(@@schema))
115
- only_children = {}
116
- MU::Cloud.resource_types.each_pair { |classname, attrs|
117
- MU::Cloud.supportedClouds.each { |cloud|
118
- begin
119
- require "mu/clouds/#{cloud.downcase}/#{attrs[:cfg_name]}"
120
- rescue LoadError => e
121
- next
122
- end
123
- res_class = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get(classname)
124
- required, res_schema = res_class.schema(self)
125
- docschema["properties"][attrs[:cfg_plural]]["items"]["description"] ||= ""
126
- docschema["properties"][attrs[:cfg_plural]]["items"]["description"] += "\n#\n# `#{cloud}`: "+res_class.quality
127
- res_schema.each { |key, cfg|
128
- if !docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]
129
- only_children[attrs[:cfg_plural]] ||= {}
130
- only_children[attrs[:cfg_plural]][key] ||= {}
131
- only_children[attrs[:cfg_plural]][key][cloud] = cfg
132
- end
133
- }
134
- }
135
- }
136
-
137
- # recursively chase down description fields in arrays and objects of our
138
- # schema and prepend stuff to them for documentation
139
- def self.prepend_descriptions(prefix, cfg)
140
- cfg["prefix"] = prefix
141
- if cfg["type"] == "array" and cfg["items"]
142
- cfg["items"] = prepend_descriptions(prefix, cfg["items"])
143
- elsif cfg["type"] == "object" and cfg["properties"]
144
- cfg["properties"].each_pair { |key, subcfg|
145
- cfg["properties"][key] = prepend_descriptions(prefix, cfg["properties"][key])
146
- }
147
- end
148
- cfg
149
- end
150
-
151
- MU::Cloud.resource_types.each_pair { |classname, attrs|
152
- MU::Cloud.supportedClouds.each { |cloud|
153
- res_class = nil
154
- begin
155
- res_class = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get(classname)
156
- rescue MU::Cloud::MuCloudResourceNotImplemented
157
- next
158
- end
159
- required, res_schema = res_class.schema(self)
160
- next if required.size == 0 and res_schema.size == 0
161
- res_schema.each { |key, cfg|
162
- cfg["description"] ||= ""
163
- if !cfg["description"].empty?
164
- cfg["description"] = "\n# +"+cloud.upcase+"+: "+cfg["description"]
165
- end
166
- if docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]
167
- schemaMerge(docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key], cfg, cloud)
168
- docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]["description"] ||= ""
169
- docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]["description"] += "\n"+(cfg["description"].match(/^#/) ? "" : "# ")+cfg["description"]
170
- MU.log "Munging #{cloud}-specific #{classname.to_s} schema into BasketofKittens => #{attrs[:cfg_plural]} => #{key}", MU::DEBUG, details: docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]
171
- else
172
- if only_children[attrs[:cfg_plural]][key]
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
175
- cfg = prepend_descriptions(prefix, cfg)
176
- end
177
-
178
- docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key] = cfg
179
- end
180
- docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]["clouds"] = {}
181
- docschema["properties"][attrs[:cfg_plural]]["items"]["properties"][key]["clouds"][cloud] = cfg
182
- }
183
-
184
- docschema['required'].concat(required)
185
- docschema['required'].uniq!
186
- }
187
- }
188
-
189
- docschema
190
- end
191
-
192
53
  attr_reader :config
193
54
 
194
55
  @@parameters = {}
@@ -216,7 +77,7 @@ module MU
216
77
  if config.is_a?(Hash)
217
78
  newhash = {}
218
79
  config.each_pair { |key, val|
219
- next if remove_runtime_keys and key.match(/^#MU_/)
80
+ next if remove_runtime_keys and (key.nil? or key.match(/^#MU_/))
220
81
  next if val.is_a?(Array) and val.empty?
221
82
  newhash[key] = self.manxify(val, remove_runtime_keys: remove_runtime_keys)
222
83
  }
@@ -243,495 +104,16 @@ module MU
243
104
  MU::Config.manxify(Marshal.load(Marshal.dump(MU.structToHash(config.dup))), remove_runtime_keys: true)
244
105
  end
245
106
 
246
- # A wrapper class for resources to refer to other resources, whether they
247
- # be a sibling object in the current deploy, an object in another deploy,
248
- # or a plain cloud id from outside of Mu.
249
- class Ref
250
- attr_reader :name
251
- attr_reader :type
252
- attr_reader :cloud
253
- attr_reader :deploy_id
254
- attr_reader :region
255
- attr_reader :credentials
256
- attr_reader :habitat
257
- attr_reader :mommacat
258
- attr_reader :tag_key
259
- attr_reader :tag_value
260
- attr_reader :obj
261
-
262
- @@refs = []
263
- @@ref_semaphore = Mutex.new
264
-
265
- # 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.
266
- # @param cfg [Hash]:
267
- # @return [MU::Config::Ref]
268
- def self.get(cfg)
269
- return cfg if cfg.is_a?(MU::Config::Ref)
270
- checkfields = cfg.keys.map { |k| k.to_sym }
271
- required = [:id, :type]
272
-
273
- @@ref_semaphore.synchronize {
274
- match = nil
275
- @@refs.each { |ref|
276
- saw_mismatch = false
277
- saw_match = false
278
- needed_values = []
279
- checkfields.each { |field|
280
- next if !cfg[field]
281
- ext_value = ref.instance_variable_get("@#{field.to_s}".to_sym)
282
- if !ext_value
283
- needed_values << field
284
- next
285
- end
286
- if cfg[field] != ext_value
287
- saw_mismatch = true
288
- elsif required.include?(field) and cfg[field] == ext_value
289
- saw_match = true
290
- end
291
- }
292
- if saw_match and !saw_mismatch
293
- # populate empty fields we got from this request
294
- if needed_values.size > 0
295
- newref = ref.dup
296
- needed_values.each { |field|
297
- newref.instance_variable_set("@#{field.to_s}".to_sym, cfg[field])
298
- if !newref.respond_to?(field)
299
- newref.singleton_class.instance_eval { attr_reader field.to_sym }
300
- end
301
- }
302
- @@refs << newref
303
- return newref
304
- else
305
- return ref
306
- end
307
- end
308
- }
309
-
310
- }
311
-
312
- # if we get here, there was no match
313
- newref = MU::Config::Ref.new(cfg)
314
- @@ref_semaphore.synchronize {
315
- @@refs << newref
316
- return newref
317
- }
318
- end
319
-
320
- # A way of dynamically defining +attr_reader+ without leaking memory
321
- def self.define_reader(name)
322
- define_method(name) {
323
- instance_variable_get("@#{name.to_s}")
324
- }
325
- end
326
-
327
- # @param cfg [Hash]: A Basket of Kittens configuration hash containing
328
- # lookup information for a cloud object
329
- def initialize(cfg)
330
- cfg.keys.each { |field|
331
- next if field == "tag"
332
- if !cfg[field].nil?
333
- self.instance_variable_set("@#{field}".to_sym, cfg[field])
334
- elsif !cfg[field.to_sym].nil?
335
- self.instance_variable_set("@#{field.to_s}".to_sym, cfg[field.to_sym])
336
- end
337
- MU::Config::Ref.define_reader(field)
338
- }
339
- if cfg['tag'] and cfg['tag']['key'] and
340
- !cfg['tag']['key'].empty? and cfg['tag']['value']
341
- @tag_key = cfg['tag']['key']
342
- @tag_value = cfg['tag']['value']
343
- end
344
-
345
- if @deploy_id and !@mommacat
346
- @mommacat = MU::MommaCat.getLitter(@deploy_id, set_context_to_me: false)
347
- elsif @mommacat and !@deploy_id
348
- @deploy_id = @mommacat.deploy_id
349
- end
350
-
351
- kitten(shallow: true) if @mommacat # try to populate the actual cloud object for this
352
- end
353
-
354
- # Comparison operator
355
- def <=>(other)
356
- return 1 if other.nil?
357
- self.to_s <=> other.to_s
358
- end
359
-
360
- # 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}.
361
- # @param aliases [Array<Hash>]: Key => value mappings to set backwards-compatibility aliases for attributes, such as the ubiquitous +vpc_id+ (+vpc_id+ => +id+).
362
- # @return [Hash]
363
- def self.schema(aliases = [], type: nil, parent_obj: nil, desc: nil, omit_fields: [])
364
- parent_obj ||= caller[1].gsub(/.*?\/([^\.\/]+)\.rb:.*/, '\1')
365
- desc ||= "Reference a #{type ? "'#{type}' resource" : "resource" } from this #{parent_obj ? "'#{parent_obj}'" : "" } resource"
366
- schema = {
367
- "type" => "object",
368
- "#MU_REFERENCE" => true,
369
- "minProperties" => 1,
370
- "description" => desc,
371
- "properties" => {
372
- "id" => {
373
- "type" => "string",
374
- "description" => "Cloud identifier of a resource we want to reference, typically used when leveraging resources not managed by MU"
375
- },
376
- "name" => {
377
- "type" => "string",
378
- "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+."
379
- },
380
- "type" => {
381
- "type" => "string",
382
- "description" => "The resource type we're attempting to reference.",
383
- "enum" => MU::Cloud.resource_types.values.map { |t| t[:cfg_plural] }
384
- },
385
- "deploy_id" => {
386
- "type" => "string",
387
- "description" => "Our target resource should be found in this Mu deploy."
388
- },
389
- "credentials" => MU::Config.credentials_primitive,
390
- "region" => MU::Config.region_primitive,
391
- "cloud" => MU::Config.cloud_primitive,
392
- "tag" => {
393
- "type" => "object",
394
- "description" => "If the target resource supports tagging and our resource implementations +find+ method supports it, we can attempt to locate it by tag.",
395
- "properties" => {
396
- "key" => {
397
- "type" => "string",
398
- "description" => "The tag or label key to search against"
399
- },
400
- "value" => {
401
- "type" => "string",
402
- "description" => "The tag or label value to match"
403
- }
404
- }
405
- }
406
- }
407
- }
408
- if !["folders", "habitats"].include?(type)
409
- schema["properties"]["habitat"] = MU::Config::Habitat.reference
410
- end
411
-
412
- if omit_fields
413
- omit_fields.each { |f|
414
- schema["properties"].delete(f)
415
- }
416
- end
417
-
418
- if !type.nil?
419
- schema["required"] = ["type"]
420
- schema["properties"]["type"]["default"] = type
421
- schema["properties"]["type"]["enum"] = [type]
422
- end
423
-
424
- aliases.each { |a|
425
- a.each_pair { |k, v|
426
- if schema["properties"][v]
427
- schema["properties"][k] = schema["properties"][v].dup
428
- schema["properties"][k]["description"] = "Alias for <tt>#{v}</tt>"
429
- else
430
- MU.log "Reference schema alias #{k} wants to alias #{v}, but no such attribute exists", MU::WARN, details: caller[4]
431
- end
432
- }
433
- }
434
-
435
- schema
436
- end
437
-
438
- # Decompose into a plain-jane {MU::Config::BasketOfKittens} hash fragment,
439
- # of the sort that would have been used to declare this reference in the
440
- # first place.
441
- def to_h
442
- me = { }
443
-
444
- self.instance_variables.each { |var|
445
- next if [:@obj, :@mommacat, :@tag_key, :@tag_value].include?(var)
446
- val = self.instance_variable_get(var)
447
- next if val.nil?
448
- val = val.to_h if val.is_a?(MU::Config::Ref)
449
- me[var.to_s.sub(/^@/, '')] = val
450
- }
451
- if @tag_key and !@tag_key.empty?
452
- me['tag'] = {
453
- 'key' => @tag_key,
454
- 'value' => @tag_value
455
- }
456
- end
457
- me
458
- end
459
-
460
- # Getter for the #{id} instance variable that attempts to populate it if
461
- # it's not set.
462
- # @return [String,nil]
463
- def id
464
- return @id if @id
465
- kitten # if it's not defined, attempt to define it
466
- @id
467
- end
468
-
469
- # Alias for {id}
470
- # @return [String,nil]
471
- def cloud_id
472
- id
473
- end
474
-
475
- # Return a {MU::Cloud} object for this reference. This is only meant to be
476
- # called in a live deploy, which is to say that if called during initial
477
- # configuration parsing, results may be incorrect.
478
- # @param mommacat [MU::MommaCat]: A deploy object which will be searched for the referenced resource if provided, before restoring to broader, less efficient searches.
479
- def kitten(mommacat = @mommacat, shallow: false)
480
- return nil if !@cloud or !@type
481
-
482
- if @obj
483
- @deploy_id ||= @obj.deploy_id
484
- @id ||= @obj.cloud_id
485
- @name ||= @obj.config['name']
486
- return @obj
487
- end
488
-
489
- if mommacat
490
- @obj = mommacat.findLitterMate(type: @type, name: @name, cloud_id: @id, credentials: @credentials, debug: false)
491
- if @obj # initialize missing attributes, if we can
492
- @id ||= @obj.cloud_id
493
- @mommacat ||= mommacat
494
- @obj.intoDeploy(@mommacat) # make real sure these are set
495
- @deploy_id ||= mommacat.deploy_id
496
- if !@name
497
- if @obj.config and @obj.config['name']
498
- @name = @obj.config['name']
499
- elsif @obj.mu_name
500
- if @type == "folders"
501
- MU.log "would assign name '#{@obj.mu_name}' in ref to this folder if I were feeling aggressive", MU::WARN, details: self.to_h
502
- end
503
- # @name = @obj.mu_name
504
- end
505
- end
506
- return @obj
507
- else
508
- # MU.log "Failed to find a live '#{@type.to_s}' object named #{@name}#{@id ? " (#{@id})" : "" }#{ @habitat ? " in habitat #{@habitat}" : "" }", MU::WARN, details: self
509
- end
510
- end
511
-
512
- if !@obj and !(@cloud == "Google" and @id and @type == "users" and MU::Cloud::Google::User.cannedServiceAcctName?(@id)) and !shallow
513
-
514
- begin
515
- hab_arg = if @habitat.nil?
516
- [nil]
517
- elsif @habitat.is_a?(MU::Config::Ref)
518
- [@habitat.id]
519
- elsif @habitat.is_a?(Hash)
520
- [@habitat["id"]]
521
- else
522
- [@habitat.to_s]
523
- end
524
-
525
- found = MU::MommaCat.findStray(
526
- @cloud,
527
- @type,
528
- name: @name,
529
- cloud_id: @id,
530
- deploy_id: @deploy_id,
531
- region: @region,
532
- habitats: hab_arg,
533
- credentials: @credentials,
534
- dummy_ok: (["habitats", "folders", "users", "groups", "vpcs"].include?(@type))
535
- )
536
- @obj ||= found.first if found
537
- rescue ThreadError => e
538
- # Sometimes MommaCat calls us in a potential deadlock situation;
539
- # don't be the cause of a fatal error if so, we don't need this
540
- # object that badly.
541
- raise e if !e.message.match(/recursive locking/)
542
- rescue SystemExit => e
543
- # XXX this is temporary, to cope with some debug stuff that's in findStray
544
- # for the nonce
545
- return
546
- end
547
- end
548
-
549
- if @obj
550
- @deploy_id ||= @obj.deploy_id
551
- @id ||= @obj.cloud_id
552
- @name ||= @obj.config['name']
553
- end
554
-
555
- @obj
556
- end
557
-
558
- end
559
-
560
- # A wrapper for config leaves that came from ERB parameters instead of raw
561
- # YAML or JSON. Will behave like a string for things that expect that
562
- # sort of thing. Code that needs to know that this leaf was the result of
563
- # a parameter will be able to tell by the object class being something
564
- # other than a plain string, array, or hash.
565
- class Tail
566
- @value = nil
567
- @name = nil
568
- @prettyname = nil
569
- @description = nil
570
- @prefix = ""
571
- @suffix = ""
572
- @is_list_element = false
573
- @pseudo = false
574
- @runtimecode = nil
575
- @valid_values = []
576
- @index = 0
577
- attr_reader :description
578
- attr_reader :pseudo
579
- attr_reader :index
580
- attr_reader :value
581
- attr_reader :runtimecode
582
- attr_reader :valid_values
583
- attr_reader :is_list_element
584
-
585
- def initialize(name, value, prettyname = nil, cloudtype = "String", valid_values = [], description = "", is_list_element = false, prefix: "", suffix: "", pseudo: false, runtimecode: nil, index: 0)
586
- @name = name
587
- @bindings = {}
588
- @value = value
589
- @valid_values = valid_values
590
- @pseudo = pseudo
591
- @index = index
592
- @runtimecode = runtimecode
593
- @cloudtype = cloudtype
594
- @is_list_element = is_list_element
595
- @description ||=
596
- if !description.nil?
597
- description
598
- else
599
- ""
600
- end
601
- @prettyname ||=
602
- if !prettyname.nil?
603
- prettyname
604
- else
605
- @name.capitalize.gsub(/[^a-z0-9]/i, "")
606
- end
607
- @prefix = prefix if !prefix.nil?
608
- @suffix = suffix if !suffix.nil?
609
- end
610
-
611
- # Return the parameter name of this Tail
612
- def getName
613
- @name
614
- end
615
- # Return the platform-specific cloud type of this Tail
616
- def getCloudType
617
- @cloudtype
618
- end
619
- # Return the human-friendly name of this Tail
620
- def getPrettyName
621
- @prettyname
622
- end
623
- # Walk like a String
624
- def to_s
625
- @prefix.to_s+@value.to_s+@suffix.to_s
626
- end
627
- # Quack like a String
628
- def to_str
629
- to_s
630
- end
631
- # Upcase like a String
632
- def upcase
633
- to_s.upcase
634
- end
635
- # Downcase like a String
636
- def downcase
637
- to_s.downcase
638
- end
639
- # Check for emptiness like a String
640
- def empty?
641
- to_s.empty?
642
- end
643
- # Match like a String
644
- def match(*args)
645
- to_s.match(*args)
646
- end
647
- # Check for equality like a String
648
- def ==(o)
649
- (o.class == self.class or o.class == "String") && o.to_s == to_s
650
- end
651
- # Concatenate like a string
652
- def +(o)
653
- return to_s if o.nil?
654
- to_s + o.to_s
655
- end
656
- # Perform global substitutions like a String
657
- def gsub(*args)
658
- to_s.gsub(*args)
659
- end
660
- end
661
-
662
- # Wrapper method for creating a {MU::Config::Tail} object as a reference to
663
- # a parameter that's valid in the loaded configuration.
664
- # @param param [<String>]: The name of the parameter to which this should be tied.
665
- # @param value [<String>]: The value of the parameter to return when asked
666
- # @param prettyname [<String>]: A human-friendly parameter name to be used when generating CloudFormation templates and the like
667
- # @param cloudtype [<String>]: A platform-specific identifier used by cloud layers to identify a parameter's type, e.g. AWS::EC2::VPC::Id
668
- # @param valid_values [Array<String>]: A list of acceptable String values for the given parameter.
669
- # @param description [<String>]: A long-form description of what the parameter does.
670
- # @param list_of [<String>]: Indicates that the value should be treated as a member of a list (array) by the cloud layer.
671
- # @param prefix [<String>]: A static String that should be prefixed to the stored value when queried
672
- # @param suffix [<String>]: A static String that should be appended to the stored value when queried
673
- # @param pseudo [<Boolean>]: This is a pseudo-parameter, automatically provided, and not available as user input.
674
- # @param runtimecode [<String>]: Actual code to allow the cloud layer to interpret literally in its own idiom, e.g. '"Ref" : "AWS::StackName"' for CloudFormation
675
- def getTail(param, value: nil, prettyname: nil, cloudtype: "String", valid_values: [], description: nil, list_of: nil, prefix: "", suffix: "", pseudo: false, runtimecode: nil)
676
- if value.nil?
677
- if @@parameters.nil? or !@@parameters.has_key?(param)
678
- MU.log "Parameter '#{param}' (#{param.class.name}) referenced in config but not provided (#{caller[0]})", MU::DEBUG, details: @@parameters
679
- return nil
680
- # raise DeployParamError
681
- else
682
- value = @@parameters[param]
683
- end
684
- end
685
- if !prettyname.nil?
686
- prettyname.gsub!(/[^a-z0-9]/i, "") # comply with CloudFormation restrictions
687
- end
688
- if value.is_a?(MU::Config::Tail)
689
- MU.log "Parameter #{param} is using a nested parameter as a value. This rarely works, depending on the target cloud. YMMV.", MU::WARN
690
- tail = MU::Config::Tail.new(param, value, prettyname, cloudtype, valid_values, description, prefix: prefix, suffix: suffix, pseudo: pseudo, runtimecode: runtimecode)
691
- elsif !list_of.nil? or (@@tails.has_key?(param) and @@tails[param].is_a?(Array))
692
- tail = []
693
- count = 0
694
- value.split(/\s*,\s*/).each { |subval|
695
- if @@tails.has_key?(param) and !@@tails[param][count].nil?
696
- subval = @@tails[param][count].values.first.to_s if subval.nil?
697
- list_of = @@tails[param][count].values.first.getName if list_of.nil?
698
- prettyname = @@tails[param][count].values.first.getPrettyName if prettyname.nil?
699
- description = @@tails[param][count].values.first.description if description.nil?
700
- valid_values = @@tails[param][count].values.first.valid_values if valid_values.nil? or valid_values.empty?
701
- cloudtype = @@tails[param][count].values.first.getCloudType if @@tails[param][count].values.first.getCloudType != "String"
702
- end
703
- prettyname = param.capitalize if prettyname.nil?
704
- tail << { list_of => MU::Config::Tail.new(list_of, subval, prettyname, cloudtype, valid_values, description, true, pseudo: pseudo, index: count) }
705
- count = count + 1
706
- }
707
- else
708
- if @@tails.has_key?(param)
709
- pseudo = @@tails[param].pseudo
710
- value = @@tails[param].to_s if value.nil?
711
- prettyname = @@tails[param].getPrettyName if prettyname.nil?
712
- description = @@tails[param].description if description.nil?
713
- valid_values = @@tails[param].valid_values if valid_values.nil? or valid_values.empty?
714
- cloudtype = @@tails[param].getCloudType if @@tails[param].getCloudType != "String"
715
- end
716
- tail = MU::Config::Tail.new(param, value, prettyname, cloudtype, valid_values, description, prefix: prefix, suffix: suffix, pseudo: pseudo, runtimecode: runtimecode)
717
- end
718
-
719
- if valid_values and valid_values.size > 0 and value
720
- if !valid_values.include?(value)
721
- raise DeployParamError, "Invalid parameter value '#{value}' supplied for '#{param}'"
722
- end
723
- end
724
- @@tails[param] = tail
725
-
726
- tail
727
- end
728
-
729
107
  # Load up our YAML or JSON and parse it through ERB, optionally substituting
730
108
  # externally-supplied parameters.
731
109
  def resolveConfig(path: @@config_path, param_pass: false, cloud: nil)
732
110
  config = nil
733
111
  @param_pass = param_pass
734
112
 
113
+ if cloud
114
+ MU.log "Exposing cloud variable to ERB with value of #{cloud}", MU::DEBUG
115
+ end
116
+
735
117
  # Catch calls to missing variables in Basket of Kittens files when being
736
118
  # parsed by ERB, and replace with placeholders for parameters. This
737
119
  # method_missing is only defined innside {MU::Config.resolveConfig}
@@ -798,8 +180,19 @@ return
798
180
  $file_format = MU::Config.guessFormat(path)
799
181
  $yaml_refs = {}
800
182
  erb = ERB.new(File.read(path), nil, "<>")
183
+ erb.filename = path
801
184
 
802
- raw_text = erb.result(erb_binding)
185
+ begin
186
+ raw_text = erb.result(erb_binding)
187
+ rescue NameError => e
188
+ loc = e.backtrace[0].sub(/:(\d+):.*/, ':\1')
189
+ msg = if e.message.match(/wrong constant name Config.getTail PLACEHOLDER ([^\s]+) REDLOHECALP/)
190
+ "Variable '#{Regexp.last_match[1]}' referenced in config, but not defined. Missing required parameter?"
191
+ else
192
+ e.message
193
+ end
194
+ raise ValidationError, msg+" at "+loc
195
+ end
803
196
  raw_json = nil
804
197
 
805
198
  # If we're working in YAML, do some magic to make includes work better.
@@ -862,17 +255,15 @@ return
862
255
  # @param cloud [String]: Sets a parameter named 'cloud', and insert it as the default cloud platform if not already declared
863
256
  # @return [Hash]: The complete validated configuration for a deployment.
864
257
  def initialize(path, skipinitialupdates = false, params: {}, updating: nil, default_credentials: nil, cloud: nil)
865
- $myPublicIp = MU::Cloud::AWS.getAWSMetaData("public-ipv4")
866
- $myRoot = MU.myRoot
258
+ $myPublicIp ||= MU.mu_public_ip
259
+ $myRoot ||= MU.myRoot
867
260
  $myRoot.freeze
868
261
 
869
- $myAZ = MU.myAZ.freeze
262
+ $myAZ ||= MU.myAZ.freeze
870
263
  $myAZ.freeze
871
- $myRegion = MU.curRegion.freeze
264
+ $myRegion ||= MU.curRegion.freeze
872
265
  $myRegion.freeze
873
266
 
874
- $myAppName = nil
875
-
876
267
  @kittens = {}
877
268
  @kittencfg_semaphore = Mutex.new
878
269
  @@config_path = path
@@ -920,7 +311,7 @@ return
920
311
  # you can't specify parameters in an included file, because ERB is what's
921
312
  # doing the including, and parameters need to already be resolved so that
922
313
  # ERB can use them.
923
- param_cfg, raw_erb_params_only = resolveConfig(path: @@config_path, param_pass: true, cloud: cloud)
314
+ param_cfg, _raw_erb_params_only = resolveConfig(path: @@config_path, param_pass: true, cloud: cloud)
924
315
  if param_cfg.has_key?("parameters")
925
316
  param_cfg["parameters"].each { |param|
926
317
  if param.has_key?("default") and param["default"].nil?
@@ -976,7 +367,7 @@ return
976
367
  $parameters = @@parameters.dup
977
368
  $parameters.freeze
978
369
 
979
- tmp_cfg, raw_erb = resolveConfig(path: @@config_path, cloud: cloud)
370
+ tmp_cfg, _raw_erb = resolveConfig(path: @@config_path, cloud: cloud)
980
371
 
981
372
  # Convert parameter entries that constitute whole config keys into
982
373
  # {MU::Config::Tail} objects.
@@ -1022,8 +413,6 @@ return
1022
413
  exit 1
1023
414
  end
1024
415
 
1025
- types = MU::Cloud.resource_types.values.map { |v| v[:cfg_plural] }
1026
-
1027
416
  MU::Cloud.resource_types.values.map { |v| v[:cfg_plural] }.each { |type|
1028
417
  if @config[type]
1029
418
  @config[type].each { |k|
@@ -1041,162 +430,37 @@ return
1041
430
  @config.freeze
1042
431
  end
1043
432
 
1044
- # Output the dependencies of this BoK stack as a directed acyclic graph.
1045
- # Very useful for debugging.
1046
- def visualizeDependencies
1047
- # GraphViz won't like MU::Config::Tail, pare down to plain Strings
1048
- config = MU::Config.stripConfig(@config)
1049
- begin
1050
- g = GraphViz.new(:G, :type => :digraph)
1051
- # Generate a GraphViz node for each resource in this stack
1052
- nodes = {}
1053
- MU::Cloud.resource_types.each_pair { |classname, attrs|
1054
- nodes[attrs[:cfg_name]] = {}
1055
- if config.has_key?(attrs[:cfg_plural]) and config[attrs[:cfg_plural]]
1056
- config[attrs[:cfg_plural]].each { |resource|
1057
- nodes[attrs[:cfg_name]][resource['name']] = g.add_nodes("#{classname}: #{resource['name']}")
1058
- }
1059
- end
1060
- }
1061
- # Now add edges corresponding to the dependencies they list
1062
- MU::Cloud.resource_types.each_pair { |classname, attrs|
1063
- if config.has_key?(attrs[:cfg_plural]) and config[attrs[:cfg_plural]]
1064
- config[attrs[:cfg_plural]].each { |resource|
1065
- if resource.has_key?("dependencies")
1066
- me = nodes[attrs[:cfg_name]][resource['name']]
1067
- resource["dependencies"].each { |dep|
1068
- parent = nodes[dep['type']][dep['name']]
1069
- g.add_edges(me, parent)
1070
- }
1071
- end
1072
- }
1073
- end
1074
- }
1075
- # Spew some output?
1076
- MU.log "Emitting dependency graph as /tmp/#{config['appname']}.jpg", MU::NOTICE
1077
- g.output(:jpg => "/tmp/#{config['appname']}.jpg")
1078
- rescue Exception => e
1079
- MU.log "Failed to generate GraphViz dependency tree: #{e.inspect}. This should only matter to developers.", MU::WARN, details: e.backtrace
1080
- end
1081
- end
1082
-
1083
- # Generate a documentation-friendly dummy Ruby class for our mu.yaml main
1084
- # config.
1085
- def self.emitConfigAsRuby
1086
- example = %Q{---
1087
- public_address: 1.2.3.4
1088
- mu_admin_email: egtlabs@eglobaltech.com
1089
- mu_admin_name: Joe Schmoe
1090
- mommacat_port: 2260
1091
- banner: My Example Mu Master
1092
- mu_repository: git://github.com/cloudamatic/mu.git
1093
- repos:
1094
- - https://github.com/cloudamatic/mu_demo_platform
1095
- allow_invade_foreign_vpcs: true
1096
- ansible_dir:
1097
- aws:
1098
- egtdev:
1099
- region: us-east-1
1100
- log_bucket_name: egt-mu-log-bucket
1101
- default: true
1102
- name: egtdev
1103
- personal:
1104
- region: us-east-2
1105
- log_bucket_name: my-mu-log-bucket
1106
- name: personal
1107
- google:
1108
- egtlabs:
1109
- project: egt-labs-admin
1110
- credentials_file: /opt/mu/etc/google.json
1111
- region: us-east4
1112
- log_bucket_name: hexabucket-761234
1113
- default: true
1114
- }
1115
- mu_yaml_schema = eval(%Q{
1116
- $NOOP = true
1117
- load "#{MU.myRoot}/bin/mu-configure"
1118
- $CONFIGURABLES
1119
- })
1120
- return if mu_yaml_schema.nil? or !mu_yaml_schema.is_a?(Hash)
1121
- muyamlpath = "#{MU.myRoot}/modules/mu/mu.yaml.rb"
1122
- MU.log "Converting mu.yaml schema to Ruby objects in #{muyamlpath}"
1123
- muyaml_rb = File.new(muyamlpath, File::CREAT|File::TRUNC|File::RDWR, 0644)
1124
- muyaml_rb.puts "# Configuration schema for mu.yaml. See also {https://github.com/cloudamatic/mu/wiki/Configuration the Mu wiki}."
1125
- muyaml_rb.puts "#"
1126
- muyaml_rb.puts "# Example:"
1127
- muyaml_rb.puts "#"
1128
- muyaml_rb.puts "# <pre>"
1129
- example.split(/\n/).each { |line|
1130
- muyaml_rb.puts "# "+line+" " # markdooooown
1131
- }
1132
- muyaml_rb.puts "# </pre>"
1133
- muyaml_rb.puts "module MuYAML"
1134
- muyaml_rb.puts "\t# The configuration file format for Mu's main config file."
1135
- self.printMuYamlSchema(muyaml_rb, [], { "subtree" => mu_yaml_schema })
1136
- muyaml_rb.puts "end"
1137
- muyaml_rb.close
1138
- end
1139
-
1140
- # Take the schema we've defined and create a dummy Ruby class tree out of
1141
- # it, basically so we can leverage Yard to document it.
1142
- def self.emitSchemaAsRuby
1143
- kittenpath = "#{MU.myRoot}/modules/mu/kittens.rb"
1144
- MU.log "Converting Basket of Kittens schema to Ruby objects in #{kittenpath}"
1145
- kitten_rb = File.new(kittenpath, File::CREAT|File::TRUNC|File::RDWR, 0644)
1146
- kitten_rb.puts "### THIS FILE IS AUTOMATICALLY GENERATED, DO NOT EDIT ###"
1147
- kitten_rb.puts "#"
1148
- kitten_rb.puts "#"
1149
- kitten_rb.puts "#"
1150
- kitten_rb.puts "module MU"
1151
- kitten_rb.puts "class Config"
1152
- kitten_rb.puts "\t# The configuration file format for Mu application stacks."
1153
- self.printSchema(kitten_rb, ["BasketofKittens"], MU::Config.docSchema)
1154
- kitten_rb.puts "end"
1155
- kitten_rb.puts "end"
1156
- kitten_rb.close
1157
-
1158
- end
1159
-
1160
- # Take an IP block and split it into a more-or-less arbitrary number of
1161
- # subnets.
1162
- # @param ip_block [String]: CIDR of the network to subdivide
1163
- # @param subnets_desired [Integer]: Number of subnets we want back
1164
- # @param max_mask [Integer]: The highest netmask we're allowed to use for a subnet (various by cloud provider)
1165
- # @return [MU::Config::Tail]: Resulting subnet tails, or nil if an error occurred.
1166
- def divideNetwork(ip_block, subnets_desired, max_mask = 28)
1167
- cidr = NetAddr::IPv4Net.parse(ip_block.to_s)
1168
-
1169
- # Ugly but reliable method of landing on the right subnet size
1170
- subnet_bits = cidr.netmask.prefix_len
1171
- begin
1172
- subnet_bits += 1
1173
- if subnet_bits > max_mask
1174
- MU.log "Can't subdivide #{cidr.to_s} into #{subnets_desired.to_s}", MU::ERR
1175
- raise MuError, "Subnets smaller than /#{max_mask} not permitted"
433
+ # Insert a dependency into the config hash of a resource, with sensible
434
+ # error checking and de-duplication.
435
+ # @param resource [Hash]
436
+ # @param name [String]
437
+ # @param type [String]
438
+ # @param phase [String]
439
+ # @param no_create_wait [Boolean]
440
+ def self.addDependency(resource, name, type, phase: "create", no_create_wait: false)
441
+ if ![nil, "create", "groom"].include?(phase)
442
+ raise MuError, "Invalid phase '#{phase}' while adding dependency #{type} #{name} to #{resource['name']}"
443
+ end
444
+ resource['dependencies'] ||= []
445
+ _shortclass, cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type)
446
+
447
+ resource['dependencies'].each { |dep|
448
+ if dep['type'] == cfg_name and dep['name'].to_s == name.to_s
449
+ dep["no_create_wait"] = no_create_wait
450
+ dep["phase"] = phase if phase
451
+ return
1176
452
  end
1177
- end while cidr.subnet_count(subnet_bits) < subnets_desired
453
+ }
1178
454
 
1179
- if cidr.subnet_count(subnet_bits) > subnets_desired
1180
- MU.log "Requested #{subnets_desired.to_s} subnets from #{cidr.to_s}, leaving #{(cidr.subnet_count(subnet_bits)-subnets_desired).to_s} unused /#{subnet_bits.to_s}s available", MU::NOTICE
1181
- end
455
+ newdep = {
456
+ "type" => cfg_name,
457
+ "name" => name.to_s,
458
+ "no_create_wait" => no_create_wait
459
+ }
460
+ newdep["phase"] = phase if phase
1182
461
 
1183
- begin
1184
- subnets = []
1185
- (0..subnets_desired).each { |x|
1186
- subnets << cidr.nth_subnet(subnet_bits, x).to_s
1187
- }
1188
- rescue RuntimeError => e
1189
- if e.message.match(/exceeds subnets available for allocation/)
1190
- MU.log e.message, MU::ERR
1191
- MU.log "I'm attempting to create #{subnets_desired} subnets (one public and one private for each Availability Zone), of #{subnet_size} addresses each, but that's too many for a /#{cidr.netmask.prefix_len} network. Either declare a larger network, or explicitly declare a list of subnets with few enough entries to fit.", MU::ERR
1192
- return nil
1193
- else
1194
- raise e
1195
- end
1196
- end
462
+ resource['dependencies'] << newdep
1197
463
 
1198
- subnets = getTail("subnetblocks", value: subnets.join(","), cloudtype: "CommaDelimitedList", description: "IP Address ranges to be used for VPC subnets", prettyname: "SubnetIpBlocks", list_of: "ip_block").map { |tail| tail["ip_block"] }
1199
- subnets
1200
464
  end
1201
465
 
1202
466
  # See if a given resource is configured in the current stack
@@ -1206,7 +470,7 @@ $CONFIGURABLES
1206
470
  def haveLitterMate?(name, type, has_multiple: false)
1207
471
  @kittencfg_semaphore.synchronize {
1208
472
  matches = []
1209
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
473
+ _shortclass, _cfg_name, cfg_plural, _classname = MU::Cloud.getResourceNames(type)
1210
474
  if @kittens[cfg_plural]
1211
475
  @kittens[cfg_plural].each { |kitten|
1212
476
  if kitten['name'].to_s == name.to_s or
@@ -1233,7 +497,7 @@ $CONFIGURABLES
1233
497
  # @param type [String]: The type of resource being removed
1234
498
  def removeKitten(name, type)
1235
499
  @kittencfg_semaphore.synchronize {
1236
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
500
+ _shortclass, _cfg_name, cfg_plural, _classname = MU::Cloud.getResourceNames(type)
1237
501
  deletia = nil
1238
502
  if @kittens[cfg_plural]
1239
503
  @kittens[cfg_plural].each { |kitten|
@@ -1247,42 +511,6 @@ $CONFIGURABLES
1247
511
  }
1248
512
  end
1249
513
 
1250
- # FirewallRules can reference other FirewallRules, which means we need to do
1251
- # an extra pass to make sure we get all intra-stack dependencies correct.
1252
- # @param acl [Hash]: The configuration hash for the FirewallRule to check
1253
- # @return [Hash]
1254
- def resolveIntraStackFirewallRefs(acl, delay_validation = false)
1255
- acl["rules"].each { |acl_include|
1256
- if acl_include['sgs']
1257
- acl_include['sgs'].each { |sg_ref|
1258
- if haveLitterMate?(sg_ref, "firewall_rules")
1259
- acl["dependencies"] ||= []
1260
- found = false
1261
- acl["dependencies"].each { |dep|
1262
- if dep["type"] == "firewall_rule" and dep["name"] == sg_ref
1263
- dep["no_create_wait"] = true
1264
- found = true
1265
- end
1266
- }
1267
- if !found
1268
- acl["dependencies"] << {
1269
- "type" => "firewall_rule",
1270
- "name" => sg_ref,
1271
- "no_create_wait" => true
1272
- }
1273
- end
1274
- siblingfw = haveLitterMate?(sg_ref, "firewall_rules")
1275
- if !siblingfw["#MU_VALIDATED"]
1276
- # XXX raise failure somehow
1277
- insertKitten(siblingfw, "firewall_rules", delay_validation: delay_validation)
1278
- end
1279
- end
1280
- }
1281
- end
1282
- }
1283
- acl
1284
- end
1285
-
1286
514
  # Insert a resource into the current stack
1287
515
  # @param descriptor [Hash]: The configuration description, as from a Basket of Kittens
1288
516
  # @param type [String]: The type of resource being added
@@ -1291,6 +519,7 @@ $CONFIGURABLES
1291
519
  def insertKitten(descriptor, type, delay_validation = false, ignore_duplicates: false, overwrite: false)
1292
520
  append = false
1293
521
  start = Time.now
522
+
1294
523
  shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
1295
524
  MU.log "insertKitten on #{cfg_name} #{descriptor['name']} (delay_validation: #{delay_validation.to_s})", MU::DEBUG, details: caller[0]
1296
525
 
@@ -1330,7 +559,7 @@ $CONFIGURABLES
1330
559
  # cloud-specific schema.
1331
560
  schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
1332
561
  myschema = Marshal.load(Marshal.dump(MU::Config.schema["properties"][cfg_plural]["items"]))
1333
- more_required, more_schema = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s).schema(self)
562
+ more_required, more_schema = MU::Cloud.resourceClass(descriptor["cloud"], type).schema(self)
1334
563
  if more_schema
1335
564
  MU::Config.schemaMerge(myschema["properties"], more_schema, descriptor["cloud"])
1336
565
  end
@@ -1349,7 +578,7 @@ $CONFIGURABLES
1349
578
  end
1350
579
 
1351
580
  # Make sure a sensible region has been targeted, if applicable
1352
- classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"])
581
+ classobj = MU::Cloud.cloudClass(descriptor["cloud"])
1353
582
  if descriptor["region"]
1354
583
  valid_regions = classobj.listRegions
1355
584
  if !valid_regions.include?(descriptor["region"])
@@ -1362,11 +591,7 @@ $CONFIGURABLES
1362
591
  if descriptor['project'].nil?
1363
592
  descriptor.delete('project')
1364
593
  elsif haveLitterMate?(descriptor['project'], "habitats")
1365
- descriptor['dependencies'] ||= []
1366
- descriptor['dependencies'] << {
1367
- "type" => "habitat",
1368
- "name" => descriptor['project']
1369
- }
594
+ MU::Config.addDependency(descriptor, descriptor['project'], "habitat")
1370
595
  end
1371
596
  end
1372
597
 
@@ -1394,21 +619,16 @@ $CONFIGURABLES
1394
619
  if !descriptor["vpc"]["name"].nil? and
1395
620
  haveLitterMate?(descriptor["vpc"]["name"], "vpcs") and
1396
621
  descriptor["vpc"]['deploy_id'].nil? and
1397
- descriptor["vpc"]['id'].nil?
1398
- descriptor["dependencies"] << {
1399
- "type" => "vpc",
1400
- "name" => descriptor["vpc"]["name"],
1401
- }
622
+ descriptor["vpc"]['id'].nil? and
623
+ !(cfg_name == "vpc" and descriptor['name'] == descriptor['vpc']['name'])
624
+ MU::Config.addDependency(descriptor, descriptor['vpc']['name'], "vpc")
1402
625
  siblingvpc = haveLitterMate?(descriptor["vpc"]["name"], "vpcs")
1403
626
 
1404
627
  if siblingvpc and siblingvpc['bastion'] and
1405
628
  ["server", "server_pool", "container_cluster"].include?(cfg_name) and
1406
629
  !descriptor['bastion']
1407
- if descriptor['name'] != siblingvpc['bastion'].to_h['name']
1408
- descriptor["dependencies"] << {
1409
- "type" => "server",
1410
- "name" => siblingvpc['bastion'].to_h['name']
1411
- }
630
+ if descriptor['name'] != siblingvpc['bastion']['name']
631
+ MU::Config.addDependency(descriptor, siblingvpc['bastion']['name'], "server")
1412
632
  end
1413
633
  end
1414
634
 
@@ -1470,7 +690,6 @@ $CONFIGURABLES
1470
690
  if (descriptor['ingress_rules'] or
1471
691
  ["server", "server_pool", "database", "cache_cluster"].include?(cfg_name))
1472
692
  descriptor['ingress_rules'] ||= []
1473
- fw_classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get("FirewallRule")
1474
693
 
1475
694
  acl = haveLitterMate?(fwname, "firewall_rules")
1476
695
  already_exists = !acl.nil?
@@ -1481,7 +700,7 @@ $CONFIGURABLES
1481
700
  "region" => descriptor['region'],
1482
701
  "credentials" => descriptor["credentials"]
1483
702
  }
1484
- if !fw_classobj.isGlobal?
703
+ if !MU::Cloud.resourceClass(descriptor["cloud"], "FirewallRule").isGlobal?
1485
704
  acl['region'] = descriptor['region']
1486
705
  acl['region'] ||= classobj.myRegion(acl['credentials'])
1487
706
  else
@@ -1507,10 +726,7 @@ $CONFIGURABLES
1507
726
  if !descriptor["loadbalancers"].nil?
1508
727
  descriptor["loadbalancers"].each { |lb|
1509
728
  if !lb["concurrent_load_balancer"].nil?
1510
- descriptor["dependencies"] << {
1511
- "type" => "loadbalancer",
1512
- "name" => lb["concurrent_load_balancer"]
1513
- }
729
+ MU::Config.addDependency(descriptor, lb["concurrent_load_balancer"], "loadbalancer")
1514
730
  end
1515
731
  }
1516
732
  end
@@ -1519,10 +735,7 @@ $CONFIGURABLES
1519
735
  if !descriptor["storage_pools"].nil?
1520
736
  descriptor["storage_pools"].each { |sp|
1521
737
  if sp["name"]
1522
- descriptor["dependencies"] << {
1523
- "type" => "storage_pool",
1524
- "name" => sp["name"]
1525
- }
738
+ MU::Config.addDependency(descriptor, sp["name"], "storage_pool")
1526
739
  end
1527
740
  }
1528
741
  end
@@ -1533,10 +746,7 @@ $CONFIGURABLES
1533
746
  next if !acl_include["name"] and !acl_include["rule_name"]
1534
747
  acl_include["name"] ||= acl_include["rule_name"]
1535
748
  if haveLitterMate?(acl_include["name"], "firewall_rules")
1536
- descriptor["dependencies"] << {
1537
- "type" => "firewall_rule",
1538
- "name" => acl_include["name"]
1539
- }
749
+ MU::Config.addDependency(descriptor, acl_include["name"], "firewall_rule", no_create_wait: (cfg_name == "vpc"))
1540
750
  elsif acl_include["name"]
1541
751
  MU.log shortclass.to_s+" #{descriptor['name']} depends on FirewallRule #{acl_include["name"]}, but no such rule declared.", MU::ERR
1542
752
  ok = false
@@ -1617,7 +827,7 @@ $CONFIGURABLES
1617
827
  plain_cfg.delete("parent_block") if cfg_plural == "vpcs"
1618
828
  begin
1619
829
  JSON::Validator.validate!(myschema, plain_cfg)
1620
- rescue JSON::Schema::ValidationError => e
830
+ rescue JSON::Schema::ValidationError
1621
831
  pp plain_cfg
1622
832
  # Use fully_validate to get the complete error list, save some time
1623
833
  errors = JSON::Validator.fully_validate(myschema, plain_cfg)
@@ -1636,7 +846,7 @@ $CONFIGURABLES
1636
846
  # Run the cloud class's deeper validation, unless we've already failed
1637
847
  # on stuff that will cause spurious alarms further in
1638
848
  if ok
1639
- parser = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s)
849
+ parser = MU::Cloud.resourceClass(descriptor['cloud'], type)
1640
850
  original_descriptor = MU::Config.stripConfig(descriptor)
1641
851
  passed = parser.validateConfig(descriptor, self)
1642
852
 
@@ -1646,7 +856,7 @@ $CONFIGURABLES
1646
856
  end
1647
857
 
1648
858
  # Make sure we've been configured with the right credentials
1649
- cloudbase = Object.const_get("MU").const_get("Cloud").const_get(descriptor['cloud'])
859
+ cloudbase = MU::Cloud.cloudClass(descriptor['cloud'])
1650
860
  credcfg = cloudbase.credConfig(descriptor['credentials'])
1651
861
  if !credcfg or credcfg.empty?
1652
862
  raise ValidationError, "#{descriptor['cloud']} #{cfg_name} #{descriptor['name']} declares credential set #{descriptor['credentials']}, but no such credentials exist for that cloud provider"
@@ -1662,168 +872,98 @@ $CONFIGURABLES
1662
872
  @kittens[cfg_plural] << descriptor if append
1663
873
  }
1664
874
 
875
+ MU.log "insertKitten completed #{cfg_name} #{descriptor['name']} in #{sprintf("%.2fs", Time.now-start)}", MU::DEBUG
876
+
1665
877
  ok
1666
878
  end
1667
879
 
1668
- @@allregions = []
1669
- MU::Cloud.availableClouds.each { |cloud|
1670
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
1671
- regions = cloudclass.listRegions()
1672
- @@allregions.concat(regions) if regions
1673
- }
880
+ # For our resources which specify intra-stack dependencies, make sure those
881
+ # dependencies are actually declared.
882
+ def check_dependencies
883
+ ok = true
1674
884
 
1675
- # Configuration chunk for choosing a provider region
1676
- # @return [Hash]
1677
- def self.region_primitive
1678
- if !@@allregions or @@allregions.empty?
1679
- @@allregions = []
1680
- MU::Cloud.availableClouds.each { |cloud|
1681
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud)
1682
- return @allregions if !cloudclass.listRegions()
1683
- @@allregions.concat(cloudclass.listRegions())
1684
- }
1685
- end
1686
- {
1687
- "type" => "string",
1688
- "enum" => @@allregions
1689
- }
1690
- end
885
+ @config.each_pair { |type, values|
886
+ next if !values.instance_of?(Array)
887
+ _shortclass, cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type, false)
888
+ next if !cfg_name
889
+ values.each { |resource|
890
+ next if !resource.kind_of?(Hash) or resource["dependencies"].nil?
891
+ addme = []
892
+ deleteme = []
893
+
894
+ resource["dependencies"].each { |dependency|
895
+ # make sure the thing we depend on really exists
896
+ sibling = haveLitterMate?(dependency['name'], dependency['type'])
897
+ if !sibling
898
+ MU.log "Missing dependency: #{type}{#{resource['name']}} needs #{cfg_name}{#{dependency['name']}}", MU::ERR
899
+ ok = false
900
+ next
901
+ end
1691
902
 
1692
- # Configuration chunk for choosing a set of cloud credentials
1693
- # @return [Hash]
1694
- def self.credentials_primitive
1695
- {
1696
- "type" => "string",
1697
- "description" => "Specify a non-default set of credentials to use when authenticating to cloud provider APIs, as listed in `mu.yaml` under each provider's subsection. If "
1698
- }
1699
- end
903
+ # Fudge dependency declarations to quash virtual_names that we know
904
+ # are extraneous. Note that wee can't do all virtual names here; we
905
+ # have no way to guess which of a collection of resources is the
906
+ # real correct one.
907
+ if sibling['virtual_name'] == dependency['name']
908
+ real_resources = []
909
+ found_exact = false
910
+ resource["dependencies"].each { |dep_again|
911
+ if dep_again['type'] == dependency['type'] and sibling['name'] == dep_again['name']
912
+ dependency['name'] = sibling['name']
913
+ found_exact = true
914
+ break
915
+ end
916
+ }
917
+ if !found_exact
918
+ all_siblings = haveLitterMate?(dependency['name'], dependency['type'], has_multiple: true)
919
+ if all_siblings.size > 0
920
+ all_siblings.each { |s|
921
+ newguy = dependency.clone
922
+ newguy['name'] = s['name']
923
+ addme << newguy
924
+ }
925
+ deleteme << dependency
926
+ MU.log "Expanding dependency which maps to virtual resources to all matching real resources", MU::NOTICE, details: { sibling['virtual_name'] => addme }
927
+ next
928
+ end
929
+ end
930
+ end
1700
931
 
1701
- # Configuration chunk for creating resource tags as an array of key/value
1702
- # pairs.
1703
- # @return [Hash]
1704
- def self.optional_tags_primitive
1705
- {
1706
- "type" => "boolean",
1707
- "description" => "Tag the resource with our optional tags (+MU-HANDLE+, +MU-MASTER-NAME+, +MU-OWNER+).",
1708
- "default" => true
1709
- }
1710
- end
932
+ # Check for a circular relationship that will lead to a deadlock
933
+ # when creating resource. This only goes one layer deep, and does
934
+ # not consider groom-phase deadlocks.
935
+ if dependency['phase'] == "groom" or dependency['no_create_wait'] or (
936
+ !MU::Cloud.resourceClass(sibling['cloud'], type).deps_wait_on_my_creation and
937
+ !MU::Cloud.resourceClass(resource['cloud'], type).waits_on_parent_completion
938
+ )
939
+ next
940
+ end
1711
941
 
1712
- # Configuration chunk for creating resource tags as an array of key/value
1713
- # pairs.
1714
- # @return [Hash]
1715
- def self.tags_primitive
1716
- {
1717
- "type" => "array",
1718
- "minItems" => 1,
1719
- "items" => {
1720
- "description" => "Tags to apply to this resource. Will apply at the cloud provider level and in node groomers, where applicable.",
1721
- "type" => "object",
1722
- "title" => "tags",
1723
- "required" => ["key", "value"],
1724
- "additionalProperties" => false,
1725
- "properties" => {
1726
- "key" => {
1727
- "type" => "string",
1728
- },
1729
- "value" => {
1730
- "type" => "string",
1731
- }
942
+ if sibling['dependencies']
943
+ sibling['dependencies'].each { |sib_dep|
944
+ next if sib_dep['type'] != cfg_name or sib_dep['no_create_wait']
945
+ cousin = haveLitterMate?(sib_dep['name'], sib_dep['type'])
946
+ if cousin and cousin['name'] == resource['name']
947
+ MU.log "Circular dependency between #{type} #{resource['name']} <=> #{dependency['type']} #{dependency['name']}", MU::ERR, details: [ resource['name'] => dependency, sibling['name'] => sib_dep ]
948
+ ok = false
949
+ end
950
+ }
951
+ end
1732
952
  }
1733
- }
1734
- }
1735
- end
953
+ resource["dependencies"].reject! { |dep| deleteme.include?(dep) }
954
+ resource["dependencies"].concat(addme)
955
+ resource["dependencies"].uniq!
1736
956
 
1737
- # Configuration chunk for choosing a cloud provider
1738
- # @return [Hash]
1739
- def self.cloud_primitive
1740
- {
1741
- "type" => "string",
1742
- # "default" => MU::Config.defaultCloud, # applyInheritedDefaults does this better
1743
- "enum" => MU::Cloud.supportedClouds
1744
- }
1745
- end
1746
-
1747
- # Generate configuration for the general-pursose ADMIN firewall rulesets
1748
- # (security groups in AWS). Note that these are unique to regions and
1749
- # individual VPCs (as well as Classic, which is just a degenerate case of
1750
- # a VPC for our purposes.
1751
- # @param vpc [Hash]: A VPC reference as defined in our config schema. This originates with the calling resource, so we'll peel out just what we need (a name or cloud id of a VPC).
1752
- # @param admin_ip [String]: Optional string of an extra IP address to allow blanket access to the calling resource.
1753
- # @param cloud [String]: The parent resource's cloud plugin identifier
1754
- # @param region [String]: Cloud provider region, if applicable.
1755
- # @return [Hash<String>]: A dependency description that the calling resource can then add to itself.
1756
- def adminFirewallRuleset(vpc: nil, admin_ip: nil, region: nil, cloud: nil, credentials: nil, rules_only: false)
1757
- if !cloud or (cloud == "AWS" and !region)
1758
- raise MuError, "Cannot call adminFirewallRuleset without specifying the parent's region and cloud provider"
1759
- end
1760
- hosts = Array.new
1761
- hosts << "#{MU.my_public_ip}/32" if MU.my_public_ip
1762
- hosts << "#{MU.my_private_ip}/32" if MU.my_private_ip
1763
- hosts << "#{MU.mu_public_ip}/32" if MU.mu_public_ip
1764
- hosts << "#{admin_ip}/32" if admin_ip
1765
- hosts.uniq!
1766
-
1767
- rules = []
1768
- if cloud == "Google"
1769
- rules = [
1770
- { "ingress" => true, "proto" => "all", "hosts" => hosts },
1771
- { "egress" => true, "proto" => "all", "hosts" => hosts }
1772
- ]
1773
- else
1774
- rules = [
1775
- { "proto" => "tcp", "port_range" => "0-65535", "hosts" => hosts },
1776
- { "proto" => "udp", "port_range" => "0-65535", "hosts" => hosts },
1777
- { "proto" => "icmp", "port_range" => "-1", "hosts" => hosts }
1778
- ]
1779
- end
1780
-
1781
- resclass = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get("FirewallRule")
1782
-
1783
- if rules_only
1784
- return rules
1785
- end
1786
-
1787
- name = "admin"
1788
- name += credentials.to_s if credentials
1789
- realvpc = nil
1790
- if vpc
1791
- realvpc = {}
1792
- ['vpc_name', 'vpc_id'].each { |p|
1793
- if vpc[p]
1794
- vpc[p.sub(/^vpc_/, '')] = vpc[p]
1795
- vpc.delete(p)
1796
- end
1797
957
  }
1798
- ['cloud', 'id', 'name', 'deploy_id', 'habitat', 'credentials'].each { |field|
1799
- realvpc[field] = vpc[field] if !vpc[field].nil?
1800
- }
1801
- if !realvpc['id'].nil? and !realvpc['id'].empty?
1802
- # Stupid kludge for Google cloud_ids which are sometimes URLs and
1803
- # sometimes not. Requirements are inconsistent from scenario to
1804
- # scenario.
1805
- name = name + "-" + realvpc['id'].gsub(/.*\//, "")
1806
- 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)
1807
- elsif !realvpc['name'].nil?
1808
- name = name + "-" + realvpc['name']
1809
- end
1810
- end
1811
-
958
+ }
1812
959
 
1813
- acl = {"name" => name, "rules" => rules, "vpc" => realvpc, "cloud" => cloud, "admin" => true, "credentials" => credentials }
1814
- if cloud == "Google" and acl["vpc"] and acl["vpc"]["habitat"]
1815
- acl['project'] = acl["vpc"]["habitat"]["id"] || acl["vpc"]["habitat"]["name"]
1816
- end
1817
- acl.delete("vpc") if !acl["vpc"]
1818
- if !resclass.isGlobal? and !region.nil? and !region.empty?
1819
- acl["region"] = region
1820
- end
1821
- @admin_firewall_rules << acl if !@admin_firewall_rules.include?(acl)
1822
- return {"type" => "firewall_rule", "name" => name}
960
+ ok
1823
961
  end
1824
962
 
1825
- private
1826
-
963
+ # Ugly text-manipulation to recursively resolve some placeholder strings
964
+ # we put in for ERB include() directives.
965
+ # @param lines [String]
966
+ # @return [String]
1827
967
  def self.resolveYAMLAnchors(lines)
1828
968
  new_text = ""
1829
969
  lines.each_line { |line|
@@ -1843,7 +983,6 @@ $CONFIGURABLES
1843
983
  return new_text
1844
984
  end
1845
985
 
1846
-
1847
986
  # Given a path to a config file, try to guess whether it's YAML or JSON.
1848
987
  # @param path [String]: The path to the file to check.
1849
988
  def self.guessFormat(path)
@@ -1852,10 +991,10 @@ $CONFIGURABLES
1852
991
  stripped = raw.gsub(/<%.*?%>,?/, "").gsub(/,[\n\s]*([\]\}])/, '\1')
1853
992
  begin
1854
993
  JSON.parse(stripped)
1855
- rescue JSON::ParserError => e
994
+ rescue JSON::ParserError
1856
995
  begin
1857
996
  YAML.load(raw.gsub(/<%.*?%>/, ""))
1858
- rescue Psych::SyntaxError => e
997
+ rescue Psych::SyntaxError
1859
998
  # Ok, well neither of those worked, let's assume that filenames are
1860
999
  # meaningful.
1861
1000
  if path.match(/\.(yaml|yml)$/i)
@@ -1935,7 +1074,7 @@ $CONFIGURABLES
1935
1074
  end
1936
1075
  begin
1937
1076
  erb = ERB.new(File.read(file), nil, "<>")
1938
- rescue Errno::ENOENT => e
1077
+ rescue Errno::ENOENT
1939
1078
  retries = retries + 1
1940
1079
  if retries == 1
1941
1080
  file = File.dirname(MU::Config.config_path)+"/"+orig_filename
@@ -1960,12 +1099,12 @@ $CONFIGURABLES
1960
1099
  parsed_cfg = nil
1961
1100
  begin
1962
1101
  parsed_cfg = JSON.parse(erb.result(binding))
1963
- parsed_as = :json
1102
+ # parsed_as = :json
1964
1103
  rescue JSON::ParserError => e
1965
1104
  MU.log e.inspect, MU::DEBUG
1966
1105
  begin
1967
1106
  parsed_cfg = YAML.load(MU::Config.resolveYAMLAnchors(erb.result(binding)))
1968
- parsed_as = :yaml
1107
+ # parsed_as = :yaml
1969
1108
  rescue Psych::SyntaxError => e
1970
1109
  MU.log e.inspect, MU::DEBUG
1971
1110
  MU.log "#{file} parsed neither as JSON nor as YAML, including as raw text", MU::WARN if @param_pass
@@ -1980,16 +1119,11 @@ $CONFIGURABLES
1980
1119
  $yaml_refs[file] = ""+YAML.dump(parsed_cfg).sub(/^---\n/, "")
1981
1120
  return "# MU::Config.include PLACEHOLDER #{file} REDLOHECALP"
1982
1121
  end
1983
- rescue SyntaxError => e
1122
+ rescue SyntaxError
1984
1123
  raise ValidationError, "ERB in #{file} threw a syntax error"
1985
1124
  end
1986
1125
  end
1987
1126
 
1988
- # (see #include)
1989
- def include(file)
1990
- MU::Config.include(file, get_binding(@@tails.keys.sort), param_pass = @param_pass)
1991
- end
1992
-
1993
1127
  @@bindings = {}
1994
1128
  # Keep a cache of bindings we've created as sandbox contexts for ERB
1995
1129
  # processing, so we don't keep reloading the entire Mu library inside new
@@ -1998,6 +1132,13 @@ $CONFIGURABLES
1998
1132
  @@bindings
1999
1133
  end
2000
1134
 
1135
+ private
1136
+
1137
+ # (see #include)
1138
+ def include(file)
1139
+ MU::Config.include(file, get_binding(@@tails.keys.sort), @param_pass)
1140
+ end
1141
+
2001
1142
  # Namespace magic to pass to ERB's result method.
2002
1143
  def get_binding(keyset)
2003
1144
  environment = $environment
@@ -2012,242 +1153,6 @@ $CONFIGURABLES
2012
1153
  MU::Config.global_bindings[keyset]
2013
1154
  end
2014
1155
 
2015
- def applySchemaDefaults(conf_chunk = config, schema_chunk = schema, depth = 0, siblings = nil, type: nil)
2016
- return if schema_chunk.nil?
2017
-
2018
- if conf_chunk != nil and schema_chunk["properties"].kind_of?(Hash) and conf_chunk.is_a?(Hash)
2019
-
2020
- if schema_chunk["properties"]["creation_style"].nil? or
2021
- schema_chunk["properties"]["creation_style"] != "existing"
2022
- schema_chunk["properties"].each_pair { |key, subschema|
2023
- shortclass = if conf_chunk[key]
2024
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(key)
2025
- shortclass
2026
- else
2027
- nil
2028
- end
2029
-
2030
- new_val = applySchemaDefaults(conf_chunk[key], subschema, depth+1, conf_chunk, type: shortclass).dup
2031
-
2032
- conf_chunk[key] = Marshal.load(Marshal.dump(new_val)) if !new_val.nil?
2033
- }
2034
- end
2035
- elsif schema_chunk["type"] == "array" and conf_chunk.kind_of?(Array)
2036
- conf_chunk.map! { |item|
2037
- # If we're working on a resource type, go get implementation-specific
2038
- # schema information so that we set those defaults correctly.
2039
- realschema = if type and schema_chunk["items"] and schema_chunk["items"]["properties"] and item["cloud"] and MU::Cloud.supportedClouds.include?(item['cloud'])
2040
-
2041
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(item["cloud"]).const_get(type)
2042
- toplevel_required, cloudschema = cloudclass.schema(self)
2043
-
2044
- newschema = schema_chunk["items"].dup
2045
- newschema["properties"].merge!(cloudschema)
2046
- newschema
2047
- else
2048
- schema_chunk["items"].dup
2049
- end
2050
-
2051
- applySchemaDefaults(item, realschema, depth+1, conf_chunk, type: type).dup
2052
- }
2053
- else
2054
- if conf_chunk.nil? and !schema_chunk["default_if"].nil? and !siblings.nil?
2055
- schema_chunk["default_if"].each { |cond|
2056
- if siblings[cond["key_is"]] == cond["value_is"]
2057
- return Marshal.load(Marshal.dump(cond["set"]))
2058
- end
2059
- }
2060
- end
2061
- if conf_chunk.nil? and schema_chunk["default"] != nil
2062
- return Marshal.load(Marshal.dump(schema_chunk["default"]))
2063
- end
2064
- end
2065
-
2066
- return conf_chunk
2067
- end
2068
-
2069
- # For our resources which specify intra-stack dependencies, make sure those
2070
- # dependencies are actually declared.
2071
- # TODO check for loops
2072
- def self.check_dependencies(config)
2073
- ok = true
2074
-
2075
- config.each_pair { |type, values|
2076
- if values.instance_of?(Array)
2077
- values.each { |resource|
2078
- if resource.kind_of?(Hash) and !resource["dependencies"].nil?
2079
- append = []
2080
- delete = []
2081
- resource["dependencies"].each { |dependency|
2082
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(dependency["type"])
2083
- found = false
2084
- names_seen = []
2085
- if !config[cfg_plural].nil?
2086
- config[cfg_plural].each { |service|
2087
- names_seen << service["name"].to_s
2088
- found = true if service["name"].to_s == dependency["name"].to_s
2089
- if service["virtual_name"]
2090
- names_seen << service["virtual_name"].to_s
2091
- if service["virtual_name"].to_s == dependency["name"].to_s
2092
- found = true
2093
- append_me = dependency.dup
2094
- append_me['name'] = service['name']
2095
- append << append_me
2096
- delete << dependency
2097
- end
2098
- end
2099
- }
2100
- end
2101
- if !found
2102
- MU.log "Missing dependency: #{type}{#{resource['name']}} needs #{cfg_name}{#{dependency['name']}}", MU::ERR, details: names_seen
2103
- ok = false
2104
- end
2105
- }
2106
- if append.size > 0
2107
- append.uniq!
2108
- resource["dependencies"].concat(append)
2109
- end
2110
- if delete.size > 0
2111
- delete.each { |delete_me|
2112
- resource["dependencies"].delete(delete_me)
2113
- }
2114
- end
2115
- end
2116
- }
2117
- end
2118
- }
2119
- return ok
2120
- end
2121
-
2122
-
2123
- # Verify that a server or server_pool has a valid AD config referencing
2124
- # valid Vaults for credentials.
2125
- def self.check_vault_refs(server)
2126
- ok = true
2127
- server['vault_access'] = [] if server['vault_access'].nil?
2128
- server['groomer'] ||= self.defaultGroomer
2129
- groomclass = MU::Groomer.loadGroomer(server['groomer'])
2130
-
2131
- begin
2132
- if !server['active_directory'].nil?
2133
- ["domain_admin_vault", "domain_join_vault"].each { |vault_class|
2134
- server['vault_access'] << {
2135
- "vault" => server['active_directory'][vault_class]['vault'],
2136
- "item" => server['active_directory'][vault_class]['item']
2137
- }
2138
- item = groomclass.getSecret(
2139
- vault: server['active_directory'][vault_class]['vault'],
2140
- item: server['active_directory'][vault_class]['item'],
2141
- )
2142
- ["username_field", "password_field"].each { |field|
2143
- if !item.has_key?(server['active_directory'][vault_class][field])
2144
- ok = false
2145
- MU.log "I don't see a value named #{field} in Chef Vault #{server['active_directory'][vault_class]['vault']}:#{server['active_directory'][vault_class]['item']}", MU::ERR
2146
- end
2147
- }
2148
- }
2149
- end
2150
-
2151
- if !server['windows_auth_vault'].nil?
2152
- server['use_cloud_provider_windows_password'] = false
2153
-
2154
- server['vault_access'] << {
2155
- "vault" => server['windows_auth_vault']['vault'],
2156
- "item" => server['windows_auth_vault']['item']
2157
- }
2158
- item = groomclass.getSecret(
2159
- vault: server['windows_auth_vault']['vault'],
2160
- item: server['windows_auth_vault']['item']
2161
- )
2162
- ["password_field", "ec2config_password_field", "sshd_password_field"].each { |field|
2163
- if !item.has_key?(server['windows_auth_vault'][field])
2164
- MU.log "No value named #{field} in Chef Vault #{server['windows_auth_vault']['vault']}:#{server['windows_auth_vault']['item']}, will use a generated password.", MU::NOTICE
2165
- server['windows_auth_vault'].delete(field)
2166
- end
2167
- }
2168
- end
2169
- # Check all of the non-special ones while we're at it
2170
- server['vault_access'].each { |v|
2171
- next if v['vault'] == "splunk" and v['item'] == "admin_user"
2172
- item = groomclass.getSecret(vault: v['vault'], item: v['item'])
2173
- }
2174
- rescue MuError
2175
- MU.log "Can't load a Chef Vault I was configured to use. Does it exist?", MU::ERR
2176
- ok = false
2177
- end
2178
- return ok
2179
- end
2180
-
2181
-
2182
- # Given a bare hash describing a resource, insert default values which can
2183
- # be inherited from its parent or from the root of the BoK.
2184
- # @param kitten [Hash]: A resource descriptor
2185
- # @param type [String]: The type of resource this is ("servers" etc)
2186
- def applyInheritedDefaults(kitten, type)
2187
- return if !kitten.is_a?(Hash)
2188
- kitten['cloud'] ||= @config['cloud']
2189
- kitten['cloud'] ||= MU::Config.defaultCloud
2190
-
2191
- if !MU::Cloud.supportedClouds.include?(kitten['cloud'])
2192
- return
2193
- end
2194
-
2195
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud'])
2196
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
2197
- resclass = Object.const_get("MU").const_get("Cloud").const_get(kitten['cloud']).const_get(shortclass)
2198
-
2199
- schema_fields = ["us_only", "scrub_mu_isms", "credentials", "billing_acct"]
2200
- if !resclass.isGlobal?
2201
- kitten['region'] ||= @config['region']
2202
- kitten['region'] ||= cloudclass.myRegion(kitten['credentials'])
2203
- schema_fields << "region"
2204
- end
2205
-
2206
- kitten['credentials'] ||= @config['credentials']
2207
- kitten['credentials'] ||= cloudclass.credConfig(name_only: true)
2208
-
2209
- kitten['us_only'] ||= @config['us_only']
2210
- kitten['us_only'] ||= false
2211
-
2212
- kitten['scrub_mu_isms'] ||= @config['scrub_mu_isms']
2213
- kitten['scrub_mu_isms'] ||= false
2214
-
2215
- if kitten['cloud'] == "Google"
2216
- # TODO this should be cloud-generic (handle AWS accounts, Azure subscriptions)
2217
- if resclass.canLiveIn.include?(:Habitat)
2218
- kitten["project"] ||= MU::Cloud::Google.defaultProject(kitten['credentials'])
2219
- schema_fields << "project"
2220
- end
2221
- if kitten['region'].nil? and !kitten['#MU_CLOUDCLASS'].nil? and
2222
- !resclass.isGlobal? and
2223
- ![MU::Cloud::VPC, MU::Cloud::FirewallRule].include?(kitten['#MU_CLOUDCLASS'])
2224
- if MU::Cloud::Google.myRegion((kitten['credentials'])).nil?
2225
- raise ValidationError, "Google '#{type}' resource '#{kitten['name']}' declared without a region, but no default Google region declared in mu.yaml under #{kitten['credentials'].nil? ? "default" : kitten['credentials']} credential set"
2226
- end
2227
- kitten['region'] ||= MU::Cloud::Google.myRegion
2228
- end
2229
- elsif kitten["cloud"] == "AWS" and !resclass.isGlobal? and !kitten['region']
2230
- if MU::Cloud::AWS.myRegion.nil?
2231
- raise ValidationError, "AWS resource declared without a region, but no default AWS region found"
2232
- end
2233
- kitten['region'] ||= MU::Cloud::AWS.myRegion
2234
- end
2235
-
2236
-
2237
- kitten['billing_acct'] ||= @config['billing_acct'] if @config['billing_acct']
2238
-
2239
- kitten["dependencies"] ||= []
2240
-
2241
- # Make sure the schema knows about these "new" fields, so that validation
2242
- # doesn't trip over them.
2243
- schema_fields.each { |field|
2244
- if @@schema["properties"][field]
2245
- MU.log "Adding #{field} to schema for #{type} #{kitten['cloud']}", MU::DEBUG, details: @@schema["properties"][field]
2246
- @@schema["properties"][type]["items"]["properties"][field] ||= @@schema["properties"][field]
2247
- end
2248
- }
2249
- end
2250
-
2251
1156
  def validate(config = @config)
2252
1157
  ok = true
2253
1158
 
@@ -2277,9 +1182,11 @@ $CONFIGURABLES
2277
1182
  }
2278
1183
  }
2279
1184
 
1185
+ newrules = []
2280
1186
  @kittens["firewall_rules"].each { |acl|
2281
- acl = resolveIntraStackFirewallRefs(acl)
1187
+ newrules << resolveIntraStackFirewallRefs(acl)
2282
1188
  }
1189
+ @kittens["firewall_rules"] = newrules
2283
1190
 
2284
1191
  # VPCs do complex things in their cloud-layer validation that other
2285
1192
  # resources tend to need, like subnet allocation, so hit them early.
@@ -2321,7 +1228,7 @@ $CONFIGURABLES
2321
1228
  ruleset = haveLitterMate?("database"+db['name'], "firewall_rules")
2322
1229
  if ruleset
2323
1230
  ["server_pools", "servers"].each { |type|
2324
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
1231
+ _shortclass, cfg_name, cfg_plural, _classname = MU::Cloud.getResourceNames(type)
2325
1232
  @kittens[cfg_plural].each { |server|
2326
1233
  server["dependencies"].each { |dep|
2327
1234
  if dep["type"] == "database" and dep["name"] == db["name"]
@@ -2331,12 +1238,7 @@ $CONFIGURABLES
2331
1238
  "port" => db["port"],
2332
1239
  "sgs" => [cfg_name+server['name']]
2333
1240
  }
2334
-
2335
- ruleset["dependencies"] << {
2336
- "name" => cfg_name+server['name'],
2337
- "type" => "firewall_rule",
2338
- "no_create_wait" => true
2339
- }
1241
+ MU::Config.addDependency(ruleset, cfg_name+server['name'], "firewall_rule", no_create_wait: true)
2340
1242
  end
2341
1243
  }
2342
1244
  }
@@ -2354,7 +1256,7 @@ $CONFIGURABLES
2354
1256
  types.each { |type|
2355
1257
  config[type] = @kittens[type] if @kittens[type].size > 0
2356
1258
  }
2357
- ok = false if !MU::Config.check_dependencies(config)
1259
+ ok = false if !check_dependencies
2358
1260
 
2359
1261
  # TODO enforce uniqueness of resource names
2360
1262
  raise ValidationError if !ok
@@ -2378,510 +1280,13 @@ $CONFIGURABLES
2378
1280
  # end
2379
1281
  end
2380
1282
 
2381
- # Emit our mu.yaml schema in a format that YARD can comprehend and turn into
2382
- # documentation.
2383
- def self.printMuYamlSchema(muyaml_rb, class_hierarchy, schema, in_array = false, required = false, prefix: nil)
2384
- return if schema.nil?
2385
- if schema["subtree"]
2386
- printme = Array.new
2387
- # order sub-elements by whether they're required, so we can use YARD's
2388
- # grouping tags on them
2389
- have_required = schema["subtree"].keys.any? { |k| schema["subtree"][k]["required"] }
2390
- prop_list = schema["subtree"].keys.sort { |a, b|
2391
- if schema["subtree"][a]["required"] and !schema["subtree"][b]["required"]
2392
- -1
2393
- elsif !schema["subtree"][a]["required"] and schema["subtree"][b]["required"]
2394
- 1
2395
- else
2396
- a <=> b
2397
- end
2398
- }
2399
-
2400
- req = false
2401
- printme << "# @!group Optional parameters" if !have_required
2402
- prop_list.each { |name|
2403
- prop = schema["subtree"][name]
2404
- if prop["required"]
2405
- printme << "# @!group Required parameters" if !req
2406
- req = true
2407
- else
2408
- if req
2409
- printme << "# @!endgroup"
2410
- printme << "# @!group Optional parameters"
2411
- end
2412
- req = false
2413
- end
2414
-
2415
- printme << self.printMuYamlSchema(muyaml_rb, class_hierarchy+ [name], prop, false, req)
2416
- }
2417
- printme << "# @!endgroup"
2418
-
2419
- desc = (schema['desc'] || schema['title'])
2420
-
2421
- tabs = 1
2422
- class_hierarchy.each { |classname|
2423
- if classname == class_hierarchy.last and desc
2424
- muyaml_rb.puts ["\t"].cycle(tabs).to_a.join('') + "# #{desc}\n"
2425
- end
2426
- muyaml_rb.puts ["\t"].cycle(tabs).to_a.join('') + "class #{classname}"
2427
- tabs = tabs + 1
2428
- }
2429
- printme.each { |lines|
2430
- if !lines.nil? and lines.is_a?(String)
2431
- lines.lines.each { |line|
2432
- muyaml_rb.puts ["\t"].cycle(tabs).to_a.join('') + line
2433
- }
2434
- end
2435
- }
2436
-
2437
- class_hierarchy.each { |classname|
2438
- tabs = tabs - 1
2439
- muyaml_rb.puts ["\t"].cycle(tabs).to_a.join('') + "end"
2440
- }
2441
-
2442
- # And now that we've dealt with our children, pass our own rendered
2443
- # commentary back up to our caller.
2444
- name = class_hierarchy.last
2445
- if in_array
2446
- type = "Array<#{class_hierarchy.join("::")}>"
2447
- else
2448
- type = class_hierarchy.join("::")
2449
- end
2450
-
2451
- docstring = "\n"
2452
- docstring = docstring + "# **REQUIRED**\n" if required
2453
- # docstring = docstring + "# **"+schema["prefix"]+"**\n" if schema["prefix"]
2454
- docstring = docstring + "# #{desc.gsub(/\n/, "\n#")}\n" if desc
2455
- docstring = docstring + "#\n"
2456
- docstring = docstring + "# @return [#{type}]\n"
2457
- docstring = docstring + "# @see #{class_hierarchy.join("::")}\n"
2458
- docstring = docstring + "attr_accessor :#{name}"
2459
- return docstring
2460
-
2461
- else
2462
- in_array = schema["array"]
2463
- name = class_hierarchy.last
2464
- type = if schema['boolean']
2465
- "Boolean"
2466
- else
2467
- "String"
2468
- end
2469
- if in_array
2470
- type = "Array<#{type}>"
2471
- end
2472
- docstring = "\n"
2473
-
2474
- prefixes = []
2475
- prefixes << "# **REQUIRED**" if schema["required"] and schema['default'].nil?
2476
- # prefixes << "# **"+schema["prefix"]+"**" if schema["prefix"]
2477
- prefixes << "# **Default: `#{schema['default']}`**" if !schema['default'].nil?
2478
- if !schema['pattern'].nil?
2479
- # XXX unquoted regex chars confuse the hell out of YARD. How do we
2480
- # quote {}[] etc in YARD-speak?
2481
- prefixes << "# **Must match pattern `#{schema['pattern'].to_s.gsub(/\n/, "\n#")}`**"
2482
- end
2483
-
2484
- desc = (schema['desc'] || schema['title'])
2485
- if prefixes.size > 0
2486
- docstring += prefixes.join(",\n")
2487
- if desc and desc.size > 1
2488
- docstring += " - "
2489
- end
2490
- docstring += "\n"
2491
- end
2492
-
2493
- docstring = docstring + "# #{desc.gsub(/\n/, "\n#")}\n" if !desc.nil?
2494
- docstring = docstring + "#\n"
2495
- docstring = docstring + "# @return [#{type}]\n"
2496
- docstring = docstring + "attr_accessor :#{name}"
2497
-
2498
- return docstring
2499
- end
2500
-
2501
- end
2502
-
2503
- # Emit our Basket of Kittens schema in a format that YARD can comprehend
2504
- # and turn into documentation.
2505
- def self.printSchema(kitten_rb, class_hierarchy, schema, in_array = false, required = false, prefix: nil)
2506
- return if schema.nil?
2507
-
2508
- if schema["type"] == "object"
2509
- printme = []
2510
-
2511
- if !schema["properties"].nil?
2512
- # order sub-elements by whether they're required, so we can use YARD's
2513
- # grouping tags on them
2514
- if !schema["required"].nil? and schema["required"].size > 0
2515
- prop_list = schema["properties"].keys.sort_by { |name|
2516
- schema["required"].include?(name) ? 0 : 1
2517
- }
2518
- else
2519
- prop_list = schema["properties"].keys
2520
- end
2521
- req = false
2522
- printme << "# @!group Optional parameters" if schema["required"].nil? or schema["required"].size == 0
2523
- prop_list.each { |name|
2524
- prop = schema["properties"][name]
2525
-
2526
- if class_hierarchy.size == 1
2527
-
2528
- _shortclass, cfg_name, cfg_plural, _classname = MU::Cloud.getResourceNames(name)
2529
- if cfg_name
2530
- example_path = MU.myRoot+"/modules/mu/config/"+cfg_name+".yml"
2531
- if File.exist?(example_path)
2532
- example = "#\n# Examples:\n#\n"
2533
- # XXX these variables are all parameters from the BoKs in
2534
- # modules/tests. A really clever implementation would read
2535
- # and parse them to get default values, perhaps, instead of
2536
- # hard-coding them here.
2537
- instance_type = "t2.medium"
2538
- db_size = "db.t2.medium"
2539
- vpc_name = "some_vpc"
2540
- logs_name = "some_loggroup"
2541
- queues_name = "some_queue"
2542
- server_pools_name = "some_server_pool"
2543
- ["simple", "complex"].each { |complexity|
2544
- erb = ERB.new(File.read(example_path), nil, "<>")
2545
- example += "# !!!yaml\n"
2546
- example += "# ---\n"
2547
- example += "# appname: #{complexity}\n"
2548
- example += "# #{cfg_plural}:\n"
2549
- firstline = true
2550
- erb.result(binding).split(/\n/).each { |l|
2551
- l.chomp!
2552
- l.sub!(/#.*/, "") if !l.match(/#(?:INTERNET|NAT|DENY)/)
2553
- next if l.empty? or l.match(/^\s+$/)
2554
- if firstline
2555
- l = "- "+l
2556
- firstline = false
2557
- else
2558
- l = " "+l
2559
- end
2560
- example += "# "+l+" "+"\n"
2561
- }
2562
- example += "# &nbsp;\n#\n" if complexity == "simple"
2563
- }
2564
- schema["properties"][name]["items"]["description"] ||= ""
2565
- if !schema["properties"][name]["items"]["description"].empty?
2566
- schema["properties"][name]["items"]["description"] += "\n"
2567
- end
2568
- schema["properties"][name]["items"]["description"] += example
2569
- end
2570
- end
2571
- end
2572
-
2573
- if !schema["required"].nil? and schema["required"].include?(name)
2574
- printme << "# @!group Required parameters" if !req
2575
- req = true
2576
- else
2577
- if req
2578
- printme << "# @!endgroup"
2579
- printme << "# @!group Optional parameters"
2580
- end
2581
- req = false
2582
- end
2583
-
2584
- printme << self.printSchema(kitten_rb, class_hierarchy+ [name], prop, false, req, prefix: schema["prefix"])
2585
- }
2586
- printme << "# @!endgroup"
2587
- end
2588
-
2589
- tabs = 1
2590
- class_hierarchy.each { |classname|
2591
- if classname == class_hierarchy.last and !schema['description'].nil?
2592
- kitten_rb.puts ["\t"].cycle(tabs).to_a.join('') + "# #{schema['description']}\n"
2593
- end
2594
- kitten_rb.puts ["\t"].cycle(tabs).to_a.join('') + "class #{classname}"
2595
- tabs = tabs + 1
2596
- }
2597
- printme.each { |lines|
2598
- if !lines.nil? and lines.is_a?(String)
2599
- lines.lines.each { |line|
2600
- kitten_rb.puts ["\t"].cycle(tabs).to_a.join('') + line
2601
- }
2602
- end
2603
- }
2604
-
2605
- class_hierarchy.each { |classname|
2606
- tabs = tabs - 1
2607
- kitten_rb.puts ["\t"].cycle(tabs).to_a.join('') + "end"
2608
- }
2609
-
2610
- # And now that we've dealt with our children, pass our own rendered
2611
- # commentary back up to our caller.
2612
- name = class_hierarchy.last
2613
- if in_array
2614
- type = "Array<#{class_hierarchy.join("::")}>"
2615
- else
2616
- type = class_hierarchy.join("::")
2617
- end
2618
-
2619
- docstring = "\n"
2620
- docstring = docstring + "# **REQUIRED**\n" if required
2621
- docstring = docstring + "# **"+schema["prefix"]+"**\n" if schema["prefix"]
2622
- docstring = docstring + "# #{schema['description'].gsub(/\n/, "\n#")}\n" if !schema['description'].nil?
2623
- docstring = docstring + "#\n"
2624
- docstring = docstring + "# @return [#{type}]\n"
2625
- docstring = docstring + "# @see #{class_hierarchy.join("::")}\n"
2626
- docstring = docstring + "attr_accessor :#{name}"
2627
- return docstring
2628
-
2629
- elsif schema["type"] == "array"
2630
- return self.printSchema(kitten_rb, class_hierarchy, schema['items'], true, required, prefix: prefix)
2631
- else
2632
- name = class_hierarchy.last
2633
- if schema['type'].nil?
2634
- MU.log "Couldn't determine schema type in #{class_hierarchy.join(" => ")}", MU::WARN, details: schema
2635
- return nil
2636
- end
2637
- if in_array
2638
- type = "Array<#{schema['type'].capitalize}>"
2639
- else
2640
- type = schema['type'].capitalize
2641
- end
2642
- docstring = "\n"
2643
-
2644
- prefixes = []
2645
- prefixes << "# **REQUIRED**" if required and schema['default'].nil?
2646
- prefixes << "# **"+schema["prefix"]+"**" if schema["prefix"]
2647
- prefixes << "# **Default: `#{schema['default']}`**" if !schema['default'].nil?
2648
- if !schema['enum'].nil? and !schema["enum"].empty?
2649
- prefixes << "# **Must be one of: `#{schema['enum'].join(', ')}`**"
2650
- elsif !schema['pattern'].nil?
2651
- # XXX unquoted regex chars confuse the hell out of YARD. How do we
2652
- # quote {}[] etc in YARD-speak?
2653
- prefixes << "# **Must match pattern `#{schema['pattern'].gsub(/\n/, "\n#")}`**"
2654
- end
2655
-
2656
- if prefixes.size > 0
2657
- docstring += prefixes.join(",\n")
2658
- if schema['description'] and schema['description'].size > 1
2659
- docstring += " - "
2660
- end
2661
- docstring += "\n"
2662
- end
2663
-
2664
- docstring = docstring + "# #{schema['description'].gsub(/\n/, "\n#")}\n" if !schema['description'].nil?
2665
- docstring = docstring + "#\n"
2666
- docstring = docstring + "# @return [#{type}]\n"
2667
- docstring = docstring + "attr_accessor :#{name}"
2668
-
2669
- return docstring
2670
- end
2671
-
2672
- end
2673
-
2674
- def self.dependencies_primitive
2675
- {
2676
- "type" => "array",
2677
- "items" => {
2678
- "type" => "object",
2679
- "description" => "Declare other objects which this resource requires. This resource will wait until the others are available to create itself.",
2680
- "required" => ["name", "type"],
2681
- "additionalProperties" => false,
2682
- "properties" => {
2683
- "name" => {"type" => "string"},
2684
- "type" => {
2685
- "type" => "string",
2686
- "enum" => MU::Cloud.resource_types.values.map { |v| v[:cfg_name] }
2687
- },
2688
- "phase" => {
2689
- "type" => "string",
2690
- "description" => "Which part of the creation process of the resource we depend on should we wait for before starting our own creation? Defaults are usually sensible, but sometimes you want, say, a Server to wait on another Server to be completely ready (through its groom phase) before starting up.",
2691
- "enum" => ["create", "groom"]
2692
- },
2693
- "no_create_wait" => {
2694
- "type" => "boolean",
2695
- "default" => false,
2696
- "description" => "By default, it's assumed that we want to wait on our parents' creation phase, in addition to whatever is declared in this stanza. Setting this flag will bypass waiting on our parent resource's creation, so that our create or groom phase can instead depend only on the parent's groom phase. "
2697
- }
2698
- }
2699
- }
2700
- }
2701
- end
2702
-
2703
- CIDR_PATTERN = "^\\d+\\.\\d+\\.\\d+\\.\\d+\/[0-9]{1,2}$"
2704
- CIDR_DESCRIPTION = "CIDR-formatted IP block, e.g. 1.2.3.4/32"
2705
- CIDR_PRIMITIVE = {
2706
- "type" => "string",
2707
- "pattern" => CIDR_PATTERN,
2708
- "description" => CIDR_DESCRIPTION
2709
- }
2710
-
2711
- # Have a default value available for config schema elements that take an
2712
- # email address.
2713
- # @return [String]
2714
- def self.notification_email
2715
- if MU.chef_user == "mu"
2716
- ENV['MU_ADMIN_EMAIL']
2717
- else
2718
- MU.userEmail
2719
- end
2720
- end
2721
-
2722
- # Load and validate the schema for an individual resource class, optionally
2723
- # merging cloud-specific schema components.
2724
- # @param type [String]: The resource type to load
2725
- # @param cloud [String]: A specific cloud, whose implementation's schema of this resource we will merge
2726
- # @return [Hash]
2727
- def self.loadResourceSchema(type, cloud: nil)
2728
- valid = true
2729
- shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
2730
- schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
2731
-
2732
- [:schema, :validate].each { |method|
2733
- if !schemaclass.respond_to?(method)
2734
- MU.log "MU::Config::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
2735
- return [nil, false] if method == :schema
2736
- valid = false
2737
- end
2738
- }
2739
-
2740
- schema = schemaclass.schema.dup
2741
-
2742
- schema["properties"]["virtual_name"] = {
2743
- "description" => "Internal use.",
2744
- "type" => "string"
2745
- }
2746
- schema["properties"]["dependencies"] = MU::Config.dependencies_primitive
2747
- schema["properties"]["cloud"] = MU::Config.cloud_primitive
2748
- schema["properties"]["credentials"] = MU::Config.credentials_primitive
2749
- schema["title"] = type.to_s
2750
-
2751
- if cloud
2752
- cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud).const_get(shortclass)
2753
-
2754
- if cloudclass.respond_to?(:schema)
2755
- reqd, cloudschema = cloudclass.schema
2756
- cloudschema.each { |key, cfg|
2757
- if schema["properties"][key]
2758
- schemaMerge(schema["properties"][key], cfg, cloud)
2759
- else
2760
- schema["properties"][key] = cfg.dup
2761
- end
2762
- }
2763
- else
2764
- MU.log "MU::Cloud::#{cloud}::#{type}.#{method.to_s} doesn't seem to be implemented", MU::ERR
2765
- valid = false
2766
- end
2767
-
2768
- end
2769
-
2770
- return [schema, valid]
2771
- end
2772
-
2773
- @@schema = {
2774
- "$schema" => "http://json-schema.org/draft-04/schema#",
2775
- "title" => "MU Application",
2776
- "type" => "object",
2777
- "description" => "A MU application stack, consisting of at least one resource.",
2778
- "required" => ["admins", "appname"],
2779
- "properties" => {
2780
- "appname" => {
2781
- "type" => "string",
2782
- "description" => "A name for your application stack. Should be short, but easy to differentiate from other applications.",
2783
- },
2784
- "scrub_mu_isms" => {
2785
- "type" => "boolean",
2786
- "description" => "When 'cloud' is set to 'CloudFormation,' use this flag to strip out Mu-specific artifacts (tags, standard userdata, naming conventions, etc) to yield a clean, source-agnostic template. Setting this flag here will override declarations in individual resources."
2787
- },
2788
- "project" => {
2789
- "type" => "string",
2790
- "description" => "**GOOGLE ONLY**: The project into which to deploy resources"
2791
- },
2792
- "billing_acct" => {
2793
- "type" => "string",
2794
- "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.",
2795
- },
2796
- "region" => MU::Config.region_primitive,
2797
- "credentials" => MU::Config.credentials_primitive,
2798
- "us_only" => {
2799
- "type" => "boolean",
2800
- "description" => "For resources which span regions, restrict to regions inside the United States",
2801
- "default" => false
2802
- },
2803
- "conditions" => {
2804
- "type" => "array",
2805
- "items" => {
2806
- "type" => "object",
2807
- "required" => ["name", "cloudcode"],
2808
- "description" => "CloudFormation-specific. Define Conditions as in http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html. Arguments must use the cloudCode() macro.",
2809
- "properties" => {
2810
- "name" => { "required" => true, "type" => "string" },
2811
- "cloudcode" => { "required" => true, "type" => "string" },
2812
- }
2813
- }
2814
- },
2815
- "parameters" => {
2816
- "type" => "array",
2817
- "items" => {
2818
- "type" => "object",
2819
- "title" => "parameter",
2820
- "description" => "Parameters to be substituted elsewhere in this Basket of Kittens as ERB variables (<%= varname %>)",
2821
- "additionalProperties" => false,
2822
- "properties" => {
2823
- "name" => { "required" => true, "type" => "string" },
2824
- "default" => { "type" => "string" },
2825
- "list_of" => {
2826
- "type" => "string",
2827
- "description" => "Treat the value as a comma-separated list of values with this key name, equivalent to CloudFormation's various List<> types. For example, set to 'subnet_id' to pass values as an array of subnet identifiers as the 'subnets' argument of a VPC stanza."
2828
- },
2829
- "prettyname" => {
2830
- "type" => "string",
2831
- "description" => "An alternative name to use when generating parameter fields in, for example, CloudFormation templates"
2832
- },
2833
- "description" => {"type" => "string"},
2834
- "cloudtype" => {
2835
- "type" => "string",
2836
- "description" => "A platform-specific string describing the type of validation to use for this parameter. E.g. when generating a CloudFormation template, set to AWS::EC2::Image::Id to validate input as an AMI identifier."
2837
- },
2838
- "required" => {
2839
- "type" => "boolean",
2840
- "default" => true
2841
- },
2842
- "valid_values" => {
2843
- "type" => "array",
2844
- "description" => "List of valid values for this parameter. Can only be a list of static strings, for now.",
2845
- "items" => {
2846
- "type" => "string"
2847
- }
2848
- }
2849
- }
2850
- }
2851
- },
2852
- # TODO availability zones (or an array thereof)
2853
-
2854
- "admins" => {
2855
- "type" => "array",
2856
- "items" => {
2857
- "type" => "object",
2858
- "title" => "admin",
2859
- "description" => "Administrative contacts for this application stack. Will be automatically set to invoking Mu user, if not specified.",
2860
- "required" => ["name", "email"],
2861
- "additionalProperties" => false,
2862
- "properties" => {
2863
- "name" => {"type" => "string"},
2864
- "email" => {"type" => "string"},
2865
- "public_key" => {
2866
- "type" => "string",
2867
- "description" => "An OpenSSH-style public key string. This will be installed on all instances created in this deployment."
2868
- }
2869
- }
2870
- },
2871
- "minItems" => 1,
2872
- "uniqueItems" => true
2873
- }
2874
- },
2875
- "additionalProperties" => false
2876
- }
2877
-
2878
1283
  failed = []
2879
1284
 
2880
1285
  # Load all of the config stub files at the Ruby level
2881
1286
  MU::Cloud.resource_types.each_pair { |type, cfg|
2882
1287
  begin
2883
1288
  require "mu/config/#{cfg[:cfg_name]}"
2884
- rescue LoadError => e
1289
+ rescue LoadError
2885
1290
  # raise MuError, "MU::Config implemention of #{type} missing from modules/mu/config/#{cfg[:cfg_name]}.rb"
2886
1291
  MU.log "MU::Config::#{type} stub class is missing", MU::ERR
2887
1292
  failed << type
@@ -2889,7 +1294,6 @@ $CONFIGURABLES
2889
1294
  end
2890
1295
  }
2891
1296
 
2892
-
2893
1297
  MU::Cloud.resource_types.each_pair { |type, cfg|
2894
1298
  begin
2895
1299
  schema, valid = loadResourceSchema(type)