cloud-mu 2.1.0beta → 3.0.0beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -39,12 +39,40 @@ module MU
39
39
  class MuCloudFlagNotImplemented < StandardError;
40
40
  end
41
41
 
42
+ # Exception we throw when we attempt to make an API call against a project
43
+ # that is already deleted.
44
+ class MuDefunctHabitat < StandardError;
45
+ end
46
+
42
47
  # Methods which a cloud resource implementation, e.g. Server, must implement
43
48
  generic_class_methods = [:find, :cleanup, :validateConfig, :schema, :isGlobal?]
44
49
  generic_instance_methods = [:create, :notify, :mu_name, :cloud_id, :config]
45
50
 
46
51
  # Class methods which the base of a cloud implementation must implement
47
- generic_class_methods_toplevel = [:required_instance_methods, :myRegion, :listRegions, :listAZs, :hosted?, :hosted_config, :config_example, :writeDeploySecret, :listCredentials, :credConfig, :listInstanceTypes, :adminBucketName, :adminBucketUrl]
52
+ generic_class_methods_toplevel = [:required_instance_methods, :myRegion, :listRegions, :listAZs, :hosted?, :hosted_config, :config_example, :writeDeploySecret, :listCredentials, :credConfig, :listInstanceTypes, :adminBucketName, :adminBucketUrl, :habitat]
53
+
54
+ # Public attributes which will be available on all instantiated cloud resource objects
55
+ #
56
+ # +:config+: The fully-resolved {MU::Config} hash describing the object, aka the Basket of Kittens entry
57
+ #
58
+ # +:mu_name+: The unique internal name of the object, if one already exists
59
+ #
60
+ # +:cloud+: The cloud in which this object is resident
61
+ #
62
+ # +:cloud_id+: The cloud provider's official identifier for this object
63
+ #
64
+ # +:environment+: The declared environment string for the deployment of which this object is a member
65
+ #
66
+ # +:deploy:+ The {MU::MommaCat} object representing the deployment of which this object is a member
67
+ #
68
+ # +:deploy_id:+ The unique string which identifies the deployment of which this object is a member
69
+ #
70
+ # +:deploydata:+ A Hash containing all metadata reported by resources in this deploy method, via their +notify+ methods
71
+ #
72
+ # +:appname:+ The declared application name of this deployment
73
+ #
74
+ # +:credentials:+ The name of the cloud provider credential set from +mu.yaml+ which is used to manage this object
75
+ PUBLIC_ATTRS = [:config, :mu_name, :cloud, :cloud_id, :environment, :deploy, :deploy_id, :deploydata, :appname, :credentials]
48
76
 
49
77
  # Initialize empty classes for each of these. We'll fill them with code
50
78
  # later; we're doing this here because otherwise the parser yells about
@@ -161,7 +189,7 @@ module MU
161
189
  :interface => self.const_get("Habitat"),
162
190
  :deps_wait_on_my_creation => true,
163
191
  :waits_on_parent_completion => true,
164
- :class => generic_class_methods,
192
+ :class => generic_class_methods + [:isLive?],
165
193
  :instance => generic_instance_methods + [:groom]
166
194
  },
167
195
  :Collection => {
@@ -217,7 +245,7 @@ module MU
217
245
  :deps_wait_on_my_creation => true,
218
246
  :waits_on_parent_completion => false,
219
247
  :class => generic_class_methods,
220
- :instance => generic_instance_methods + [:registerNode]
248
+ :instance => generic_instance_methods + [:groom, :registerNode]
221
249
  },
222
250
  :Server => {
223
251
  :has_multiples => true,
@@ -227,8 +255,8 @@ module MU
227
255
  :interface => self.const_get("Server"),
228
256
  :deps_wait_on_my_creation => false,
229
257
  :waits_on_parent_completion => false,
230
- :class => generic_class_methods + [:validateInstanceType],
231
- :instance => generic_instance_methods + [:groom, :postBoot, :getSSHConfig, :canonicalIP, :getWindowsAdminPassword, :active?, :groomer, :mu_windows_name, :mu_windows_name=, :reboot, :addVolume]
258
+ :class => generic_class_methods + [:validateInstanceType, :imageTimeStamp],
259
+ :instance => generic_instance_methods + [:groom, :postBoot, :getSSHConfig, :canonicalIP, :getWindowsAdminPassword, :active?, :groomer, :mu_windows_name, :mu_windows_name=, :reboot, :addVolume, :genericNAT]
232
260
  },
233
261
  :ServerPool => {
234
262
  :has_multiples => false,
@@ -403,8 +431,8 @@ module MU
403
431
  :interface => self.const_get("Bucket"),
404
432
  :deps_wait_on_my_creation => true,
405
433
  :waits_on_parent_completion => true,
406
- :class => generic_class_methods,
407
- :instance => generic_instance_methods + [:groom]
434
+ :class => generic_class_methods + [:upload],
435
+ :instance => generic_instance_methods + [:groom, :upload]
408
436
  },
409
437
  :NoSQLDB => {
410
438
  :has_multiples => false,
@@ -419,6 +447,195 @@ module MU
419
447
  }
420
448
  }.freeze
421
449
 
450
+ # The public AWS S3 bucket where we expect to find YAML files listing our
451
+ # standard base images for various platforms.
452
+ BASE_IMAGE_BUCKET = "cloudamatic"
453
+ # The path in the AWS S3 bucket where we expect to find YAML files listing
454
+ # our standard base images for various platforms.
455
+ BASE_IMAGE_PATH = "/images"
456
+
457
+ # Aliases for platform names, in case we don't have actual images built for
458
+ # them.
459
+ PLATFORM_ALIASES = {
460
+ "linux" => "centos7",
461
+ "windows" => "win2k12r2",
462
+ "win2k12" => "win2k12r2",
463
+ "ubuntu" => "ubuntu16",
464
+ "centos" => "centos7",
465
+ "rhel7" => "rhel71",
466
+ "rhel" => "rhel71",
467
+ "amazon" => "amazon2016"
468
+ }
469
+
470
+ @@image_fetch_cache = {}
471
+ @@platform_cache = []
472
+ @@image_fetch_semaphore = Mutex.new
473
+
474
+ # Rifle our image lists from {MU::Cloud.getStockImage} and return a list
475
+ # of valid +platform+ names.
476
+ # @return [Array<String>]
477
+ def self.listPlatforms
478
+ return @@platform_cache if @@platform_cache and !@@platform_cache.empty?
479
+ @@platform_cache = MU::Cloud.supportedClouds.map { |cloud|
480
+ begin
481
+ loadCloudType(cloud, :Server)
482
+ rescue MU::Cloud::MuCloudResourceNotImplemented, MU::MuError => e
483
+ next
484
+ end
485
+
486
+ images = MU::Cloud.getStockImage(cloud, quiet: true)
487
+ if images
488
+ images.keys
489
+ else
490
+ nil
491
+ end
492
+ }.flatten.uniq
493
+ @@platform_cache.delete(nil)
494
+ @@platform_cache.sort
495
+ @@platform_cache
496
+ end
497
+
498
+ # Locate a base image for a {MU::Cloud::Server} resource. First we check
499
+ # Mu's public bucket, which should list the latest and greatest. If we can't
500
+ # fetch that, then we fall back to a YAML file that's bundled as part of Mu,
501
+ # but which will typically be less up-to-date.
502
+ # @param cloud [String]: The cloud provider for which to return an image list
503
+ # @param platform [String]: The supported platform for which to return an image or images. If not specified, we'll return our entire library for the appropriate cloud provider.
504
+ # @param region [String]: The region for which the returned image or images should be supported, for cloud providers which require it (such as AWS).
505
+ # @param fail_hard [Boolean]: Raise an exception on most errors, such as an inability to reach our public listing, lack of matching images, etc.
506
+ # @return [Hash,String,nil]
507
+ def self.getStockImage(cloud = MU::Config.defaultCloud, platform: nil, region: nil, fail_hard: false, quiet: false)
508
+
509
+ if !MU::Cloud.supportedClouds.include?(cloud)
510
+ MU.log "'#{cloud}' is not a supported cloud provider! Available providers:", MU::ERR, details: MU::Cloud.supportedClouds
511
+ raise MuError, "'#{cloud}' is not a supported cloud provider!"
512
+ end
513
+
514
+ urls = ["http://"+BASE_IMAGE_BUCKET+".s3-website-us-east-1.amazonaws.com"+BASE_IMAGE_PATH]
515
+ if $MU_CFG and $MU_CFG['custom_images_url']
516
+ urls << $MU_CFG['custom_images_url']
517
+ end
518
+
519
+ images = nil
520
+ urls.each { |base_url|
521
+ @@image_fetch_semaphore.synchronize {
522
+ if @@image_fetch_cache[cloud] and (Time.now - @@image_fetch_cache[cloud]['time']) < 30
523
+ images = @@image_fetch_cache[cloud]['contents'].dup
524
+ else
525
+ begin
526
+ Timeout.timeout(2) do
527
+ response = open("#{base_url}/#{cloud}.yaml").read
528
+ images ||= {}
529
+ images.deep_merge!(YAML.load(response))
530
+ break
531
+ end
532
+ rescue Exception => e
533
+ if fail_hard
534
+ raise MuError, "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})"
535
+ else
536
+ MU.log "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})", MU::WARN if !quiet
537
+ end
538
+ end
539
+ end
540
+ }
541
+ }
542
+
543
+ @@image_fetch_semaphore.synchronize {
544
+ @@image_fetch_cache[cloud] = {
545
+ 'contents' => images.dup,
546
+ 'time' => Time.now
547
+ }
548
+ }
549
+
550
+ backwards_compat = {
551
+ "AWS" => "amazon_images",
552
+ "Google" => "google_images",
553
+ }
554
+
555
+ # Load from inside our repository, if we didn't get images elsewise
556
+ if images.nil?
557
+ [backwards_compat[cloud], cloud].each { |file|
558
+ next if file.nil?
559
+ if File.exist?("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml")
560
+ images = YAML.load(File.read("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml"))
561
+ break
562
+ end
563
+ }
564
+ end
565
+
566
+ # Now overlay local overrides, both of the systemwide (/opt/mu/etc) and
567
+ # per-user (~/.mu/etc) variety.
568
+ [backwards_compat[cloud], cloud].each { |file|
569
+ next if file.nil?
570
+ if File.exist?("#{MU.etcDir}/#{file}.yaml")
571
+ images ||= {}
572
+ images.deep_merge!(YAML.load(File.read("#{MU.etcDir}/#{file}.yaml")))
573
+ end
574
+ if Process.uid != 0
575
+ basepath = Etc.getpwuid(Process.uid).dir+"/.mu/etc"
576
+ if File.exist?("#{basepath}/#{file}.yaml")
577
+ images ||= {}
578
+ images.deep_merge!(YAML.load(File.read("#{basepath}/#{file}.yaml")))
579
+ end
580
+ end
581
+ }
582
+
583
+ if images.nil?
584
+ if fail_hard
585
+ raise MuError, "Failed to find any base images for #{cloud}"
586
+ else
587
+ MU.log "Failed to find any base images for #{cloud}", MU::WARN if !quiet
588
+ return nil
589
+ end
590
+ end
591
+
592
+ PLATFORM_ALIASES.each_pair { |a, t|
593
+ if images[t] and !images[a]
594
+ images[a] = images[t]
595
+ end
596
+ }
597
+
598
+ if platform
599
+ if !images[platform]
600
+ if fail_hard
601
+ raise MuError, "No base image for platform #{platform} in cloud #{cloud}"
602
+ else
603
+ MU.log "No base image for platform #{platform} in cloud #{cloud}", MU::WARN if !quiet
604
+ return nil
605
+ end
606
+ end
607
+ images = images[platform]
608
+
609
+ if region
610
+ # We won't fuss about the region argument if this isn't a cloud that
611
+ # has regions, just quietly don't bother.
612
+ if images.is_a?(Hash)
613
+ if images[region]
614
+ images = images[region]
615
+ else
616
+ if fail_hard
617
+ raise MuError, "No base image for platform #{platform} in cloud #{cloud} region #{region} found"
618
+ else
619
+ MU.log "No base image for platform #{platform} in cloud #{cloud} region #{region} found", MU::WARN if !quiet
620
+ return nil
621
+ end
622
+ end
623
+ end
624
+ end
625
+ else
626
+ if region
627
+ images.each_pair { |p, regions|
628
+ # Filter to match our requested region, but for all the platforms,
629
+ # since we didn't specify one.
630
+ if regions.is_a?(Hash)
631
+ regions.delete_if { |r| r != region }
632
+ end
633
+ }
634
+ end
635
+ end
636
+
637
+ images
638
+ end
422
639
 
423
640
  # A list of supported cloud resource types as Mu classes
424
641
  def self.resource_types;
@@ -429,6 +646,7 @@ module MU
429
646
  # @param type [String]: A string that looks like our short or full class name or singular or plural configuration names.
430
647
  # @return [Array]: Class name (Symbol), singular config name (String), plural config name (String), full class name (Object)
431
648
  def self.getResourceNames(type)
649
+ return [nil, nil, nil, nil, {}] if !type
432
650
  @@resource_types.each_pair { |name, cloudclass|
433
651
  if name == type.to_sym or
434
652
  cloudclass[:cfg_name] == type or
@@ -462,10 +680,28 @@ module MU
462
680
  @@supportedCloudList = ['AWS', 'CloudFormation', 'Google', 'Azure']
463
681
 
464
682
  # List of known/supported Cloud providers
683
+ # @return [Array<String>]
465
684
  def self.supportedClouds
466
685
  @@supportedCloudList
467
686
  end
468
687
 
688
+ # List of known/supported Cloud providers for which we have at least one
689
+ # set of credentials configured.
690
+ # @return [Array<String>]
691
+ def self.availableClouds
692
+ available = []
693
+ MU::Cloud.supportedClouds.each { |cloud|
694
+ begin
695
+ cloudbase = Object.const_get("MU").const_get("Cloud").const_get(cloud)
696
+ next if cloudbase.listCredentials.nil? or cloudbase.listCredentials.empty?
697
+ available << cloud
698
+ rescue NameError
699
+ end
700
+ }
701
+
702
+ available
703
+ end
704
+
469
705
  # Load the container class for each cloud we know about, and inject autoload
470
706
  # code for each of its supported resource type classes.
471
707
  failed = []
@@ -495,7 +731,7 @@ module MU
495
731
  # @param template_variables [Hash]: A list of variable substitutions to pass as globals to the ERB parser when loading the userdata script.
496
732
  # @param custom_append [String]: Arbitrary extra code to append to our default userdata behavior.
497
733
  # @return [String]
498
- def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, cloud: "aws", scrub_mu_isms: false)
734
+ def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, cloud: "AWS", scrub_mu_isms: false, credentials: nil)
499
735
  return nil if platform.nil? or platform.empty?
500
736
  userdata_mutex.synchronize {
501
737
  script = ""
@@ -503,10 +739,16 @@ module MU
503
739
  if template_variables.nil? or !template_variables.is_a?(Hash)
504
740
  raise MuError, "My second argument should be a hash of variables to pass into ERB templates"
505
741
  end
742
+ template_variables["credentials"] ||= credentials
506
743
  $mu = OpenStruct.new(template_variables)
507
- userdata_dir = File.expand_path(MU.myRoot+"/modules/mu/clouds/#{cloud}/userdata")
508
- platform = "linux" if %w{centos centos6 centos7 ubuntu ubuntu14 rhel rhel7 rhel71 amazon}.include? platform
509
- platform = "windows" if %w{win2k12r2 win2k12 win2k8 win2k8r2 win2k16}.include? platform
744
+ userdata_dir = File.expand_path(MU.myRoot+"/modules/mu/clouds/#{cloud.downcase}/userdata")
745
+
746
+ platform = if %w{win2k12r2 win2k12 win2k8 win2k8r2 win2k16 windows win2k19}.include?(platform)
747
+ "windows"
748
+ else
749
+ "linux"
750
+ end
751
+
510
752
  erbfile = "#{userdata_dir}/#{platform}.erb"
511
753
  if !File.exist?(erbfile)
512
754
  MU.log "No such userdata template '#{erbfile}'", MU::WARN, details: caller
@@ -590,12 +832,12 @@ module MU
590
832
  }
591
833
  @@resource_types[type.to_sym][:instance].each { |instance_method|
592
834
  if !myclass.public_instance_methods.include?(instance_method)
593
- raise MuError, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
835
+ raise MuCloudResourceNotImplemented, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
594
836
  end
595
837
  }
596
838
  cloudclass.required_instance_methods.each { |instance_method|
597
839
  if !myclass.public_instance_methods.include?(instance_method)
598
- raise MuError, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
840
+ MU.log "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}, will declare as attr_accessor", MU::DEBUG
599
841
  end
600
842
  }
601
843
 
@@ -603,7 +845,7 @@ module MU
603
845
  return myclass
604
846
  rescue NameError => e
605
847
  @cloud_class_cache[cloud][type] = nil
606
- raise MuError, "The '#{type}' resource is not supported in cloud #{cloud} (tried MU::#{cloud}::#{type})", e.backtrace
848
+ raise MuCloudResourceNotImplemented, "The '#{type}' resource is not supported in cloud #{cloud} (tried MU::#{cloud}::#{type})", e.backtrace
607
849
  end
608
850
  end
609
851
 
@@ -624,20 +866,9 @@ module MU
624
866
 
625
867
  @@resource_types.each_pair { |name, attrs|
626
868
  Object.const_get("MU").const_get("Cloud").const_get(name).class_eval {
627
- attr_reader :cloud
628
- attr_reader :environment
629
869
  attr_reader :cloudclass
630
870
  attr_reader :cloudobj
631
- attr_reader :deploy_id
632
- attr_reader :mu_name
633
- attr_reader :cloud_id
634
- attr_reader :credentials
635
- attr_reader :url
636
- attr_reader :config
637
- attr_reader :deploydata
638
871
  attr_reader :destroyed
639
- attr_reader :cfm_template
640
- attr_reader :cfm_name
641
872
  attr_reader :delayed_save
642
873
 
643
874
  def self.shortname
@@ -668,11 +899,6 @@ module MU
668
899
  MU::Cloud.resource_types[shortname.to_sym][:deps_wait_on_my_creation]
669
900
  end
670
901
 
671
- def groomer
672
- return @cloudobj.groomer if !@cloudobj.nil?
673
- nil
674
- end
675
-
676
902
  # Print something palatable when we're called in a string context.
677
903
  def to_s
678
904
  fullname = "#{self.class.shortname}"
@@ -688,86 +914,232 @@ module MU
688
914
  return fullname
689
915
  end
690
916
 
917
+ # Set our +deploy+ and +deploy_id+ attributes, optionally doing so even
918
+ # if they have already been set.
919
+ #
920
+ # @param mommacat [MU::MommaCat]: The deploy to which we're being told we belong
921
+ # @param force [Boolean]: Set even if we already have a deploy object
922
+ # @return [String]: Our new +deploy_id+
923
+ def intoDeploy(mommacat, force: false)
924
+ if force or (!@deploy)
925
+ MU.log "Inserting #{self} (#{self.object_id}) into #{mommacat.deploy_id}", MU::DEBUG
926
+ @deploy = mommacat
927
+ @deploy_id = @deploy.deploy_id
928
+ @cloudobj.intoDeploy(mommacat, force: force) if @cloudobj
929
+ end
930
+ @deploy_id
931
+ end
691
932
 
692
933
  # @param mommacat [MU::MommaCat]: The deployment containing this cloud resource
693
934
  # @param mu_name [String]: Optional- specify the full Mu resource name of an existing resource to load, instead of creating a new one
694
935
  # @param cloud_id [String]: Optional- specify the cloud provider's identifier for an existing resource to load, instead of creating a new one
695
936
  # @param kitten_cfg [Hash]: The parse configuration for this object from {MU::Config}
696
- def initialize(mommacat: nil,
697
- mu_name: nil,
698
- cloud_id: nil,
699
- credentials: nil,
700
- kitten_cfg: nil,
701
- delayed_save: false)
702
- raise MuError, "Cannot invoke Cloud objects without a configuration" if kitten_cfg.nil?
703
- @live = true
704
- @deploy = mommacat
705
- @config = kitten_cfg
706
- @delayed_save = delayed_save
707
- @cloud_id = cloud_id
708
- @credentials = credentials
709
- @credentials ||= kitten_cfg['credentials']
937
+ def initialize(**args)
938
+ raise MuError, "Cannot invoke Cloud objects without a configuration" if args[:kitten_cfg].nil?
939
+
940
+ # We are a parent wrapper object. Initialize our child object and
941
+ # housekeeping bits accordingly.
942
+ if self.class.name.match(/^MU::Cloud::([^:]+)$/)
943
+ @live = true
944
+ @delayed_save = args[:delayed_save]
945
+ @method_semaphore = Mutex.new
946
+ @method_locks = {}
947
+ if args[:mommacat]
948
+ MU.log "Initializing an instance of #{self.class.name} in #{args[:mommacat].deploy_id} #{mu_name}", MU::DEBUG, details: args[:kitten_cfg]
949
+ elsif args[:mu_name].nil?
950
+ raise MuError, "Can't instantiate a MU::Cloud object with a live deploy or giving us a mu_name"
951
+ else
952
+ MU.log "Initializing a detached #{self.class.name} named #{args[:mu_name]}", MU::DEBUG, details: args[:kitten_cfg]
953
+ end
710
954
 
711
- if !@deploy.nil?
712
- @deploy_id = @deploy.deploy_id
713
- MU.log "Initializing an instance of #{self.class.name} in #{@deploy_id} #{mu_name}", MU::DEBUG, details: kitten_cfg
714
- elsif mu_name.nil?
715
- raise MuError, "Can't instantiate a MU::Cloud object with a live deploy or giving us a mu_name"
955
+ my_cloud = args[:kitten_cfg]['cloud'].to_s || MU::Config.defaultCloud
956
+ if my_cloud.nil? or !MU::Cloud.supportedClouds.include?(my_cloud)
957
+ raise MuError, "Can't instantiate a MU::Cloud object without a valid cloud (saw '#{my_cloud}')"
958
+ end
959
+
960
+ @cloudclass = MU::Cloud.loadCloudType(my_cloud, self.class.shortname)
961
+ @cloudparentclass = Object.const_get("MU").const_get("Cloud").const_get(my_cloud)
962
+ @cloudobj = @cloudclass.new(
963
+ mommacat: args[:mommacat],
964
+ kitten_cfg: args[:kitten_cfg],
965
+ cloud_id: args[:cloud_id],
966
+ mu_name: args[:mu_name]
967
+ )
968
+ raise MuError, "Unknown error instantiating #{self}" if @cloudobj.nil?
969
+ # These should actually call the method live instead of caching a static value
970
+ PUBLIC_ATTRS.each { |a|
971
+ instance_variable_set(("@"+a.to_s).to_sym, @cloudobj.send(a))
972
+ }
973
+ @deploy ||= args[:mommacat]
974
+ @deploy_id ||= @deploy.deploy_id if @deploy
975
+
976
+ # Register with the containing deployment
977
+ if !@deploy.nil? and !@cloudobj.mu_name.nil? and
978
+ !@cloudobj.mu_name.empty? and !args[:delay_descriptor_load]
979
+ describe # XXX is this actually safe here?
980
+ @deploy.addKitten(self.class.cfg_name, @config['name'], self)
981
+ elsif !@deploy.nil? and @cloudobj.mu_name.nil?
982
+ MU.log "#{self} in #{@deploy.deploy_id} didn't generate a mu_name after being loaded/initialized, dependencies on this resource will probably be confused!", MU::ERR, details: [caller, args.keys]
983
+ end
984
+
985
+
986
+ # We are actually a child object invoking this via super() from its
987
+ # own initialize(), so initialize all the attributes and instance
988
+ # variables we know to be universal.
716
989
  else
717
- MU.log "Initializing an independent instance of #{self.class.name} named #{mu_name}", MU::DEBUG, details: kitten_cfg
718
- end
719
- if !kitten_cfg.has_key?("cloud")
720
- kitten_cfg['cloud'] = MU::Config.defaultCloud
721
- end
722
- @cloud = kitten_cfg['cloud']
723
- @cloudclass = MU::Cloud.loadCloudType(@cloud, self.class.shortname)
724
- @environment = kitten_cfg['environment']
725
- @method_semaphore = Mutex.new
726
- @method_locks = {}
727
- # XXX require subclass to provide attr_readers of @config and @deploy
728
990
 
729
- @cloudobj = @cloudclass.new(mommacat: mommacat, kitten_cfg: kitten_cfg, cloud_id: cloud_id, mu_name: mu_name)
991
+ # Declare the attributes that everyone should have
992
+ class << self
993
+ PUBLIC_ATTRS.each { |a|
994
+ attr_reader a
995
+ }
996
+ end
730
997
 
731
- raise MuError, "Unknown error instantiating #{self}" if @cloudobj.nil?
998
+ # XXX this butchers ::Id and ::Ref objects that might be used by dependencies() to good effect, but we also can't expect our implementations to cope with knowing when a .to_s has to be appended to things at random
999
+ @config = MU::Config.manxify(args[:kitten_cfg]) || MU::Config.manxify(args[:config])
732
1000
 
733
- # If we just loaded an existing object, go ahead and prepopulate the
734
- # describe() cache
735
- if !cloud_id.nil? or !mu_name.nil?
736
- @cloudobj.describe(cloud_id: cloud_id)
737
- @cloud_id ||= @cloudobj.cloud_id
738
- end
1001
+ if !@config
1002
+ MU.log "Missing config arguments in setInstanceVariables, can't initialize a cloud object without it", MU::ERR, details: args.keys
1003
+ raise MuError, "Missing config arguments in setInstanceVariables"
1004
+ end
739
1005
 
740
- @deploydata = @cloudobj.deploydata
741
- @config = @cloudobj.config
1006
+ @deploy = args[:mommacat] || args[:deploy]
742
1007
 
743
- # If we're going to be integrated into AD or otherwise need a short
744
- # hostname, generate it now.
745
- if self.class.shortname == "Server" and (@cloudobj.windows? or @config['active_directory']) and @cloudobj.mu_windows_name.nil?
746
- if !@deploydata.nil? and !@deploydata['mu_windows_name'].nil?
747
- @cloudobj.mu_windows_name = @deploydata['mu_windows_name']
748
- else
749
- # Use the same random differentiator as the "real" name if we're
750
- # from a ServerPool. Helpful for admin sanity.
751
- unq = @cloudobj.mu_name.sub(/^.*?-(...)$/, '\1')
752
- if @config['basis'] and !unq.nil? and !unq.empty?
753
- @cloudobj.mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true, use_unique_string: unq, reuse_unique_string: true)
754
- else
755
- @cloudobj.mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true)
1008
+ @credentials = args[:credentials]
1009
+ @credentials ||= @config['credentials']
1010
+
1011
+ @cloud = @config['cloud']
1012
+ if !@cloud
1013
+ if self.class.name.match(/^MU::Cloud::([^:]+)(?:::.+|$)/)
1014
+ cloudclass_name = Regexp.last_match[1]
1015
+ if MU::Cloud.supportedClouds.include?(cloudclass_name)
1016
+ @cloud = cloudclass_name
1017
+ end
756
1018
  end
757
1019
  end
758
- end
1020
+ if !@cloud
1021
+ raise MuError, "Failed to determine what cloud #{self} should be in!"
1022
+ end
1023
+
1024
+ @environment = @config['environment']
1025
+ if @deploy
1026
+ @deploy_id = @deploy.deploy_id
1027
+ @appname = @deploy.appname
1028
+ end
1029
+
1030
+ @cloudclass = MU::Cloud.loadCloudType(@cloud, self.class.shortname)
1031
+ @cloudparentclass = Object.const_get("MU").const_get("Cloud").const_get(@cloud)
1032
+
1033
+ # A pre-existing object, you say?
1034
+ if args[:cloud_id]
1035
+
1036
+ # TODO implement ::Id for every cloud... and they should know how to get from
1037
+ # cloud_desc to a fully-resolved ::Id object, not just the short string
1038
+
1039
+ @cloud_id = args[:cloud_id]
1040
+ describe(cloud_id: @cloud_id)
1041
+ @habitat_id = habitat_id # effectively, cache this
1042
+
1043
+ # If we can build us an ::Id object for @cloud_id instead of a
1044
+ # string, do so.
1045
+ begin
1046
+ idclass = Object.const_get("MU").const_get("Cloud").const_get(@cloud).const_get("Id")
1047
+ long_id = if @deploydata and @deploydata[idclass.idattr.to_s]
1048
+ @deploydata[idclass.idattr.to_s]
1049
+ elsif self.respond_to?(idclass.idattr)
1050
+ self.send(idclass.idattr)
1051
+ end
1052
+
1053
+ @cloud_id = idclass.new(long_id) if !long_id.nil? and !long_id.empty?
1054
+ # 1 see if we have the value on the object directly or in deploy data
1055
+ # 2 set an attr_reader with the value
1056
+ # 3 rewrite our @cloud_id attribute with a ::Id object
1057
+ rescue NameError, MU::Cloud::MuCloudResourceNotImplemented
1058
+ end
1059
+
1060
+ end
1061
+
1062
+ # Use pre-existing mu_name (we're probably loading an extant deploy)
1063
+ # if available
1064
+ if args[:mu_name]
1065
+ @mu_name = args[:mu_name].dup
1066
+ # If scrub_mu_isms is set, our mu_name is always just the bare name
1067
+ # field of the resource.
1068
+ elsif @config['scrub_mu_isms']
1069
+ @mu_name = @config['name'].dup
1070
+ # XXX feck it insert an inheritable method right here? Set a default? How should resource implementations determine whether they're instantiating a new object?
1071
+ end
1072
+
1073
+ @tags = {}
1074
+ if !@config['scrub_mu_isms']
1075
+ @tags = @deploy ? @deploy.listStandardTags : MU::MommaCat.listStandardTags
1076
+ end
1077
+ if @config['tags']
1078
+ @config['tags'].each { |tag|
1079
+ @tags[tag['key']] = tag['value']
1080
+ }
1081
+ end
1082
+
1083
+ if @cloudparentclass.respond_to?(:resourceInitHook)
1084
+ @cloudparentclass.resourceInitHook(self, @deploy)
1085
+ end
1086
+
1087
+ # Add cloud-specific instance methods for our resource objects to
1088
+ # inherit.
1089
+ if @cloudparentclass.const_defined?(:AdditionalResourceMethods)
1090
+ self.extend @cloudparentclass.const_get(:AdditionalResourceMethods)
1091
+ end
759
1092
 
760
- # Register us with our parent deploy so that we can be found by our
761
- # littermates if needed.
762
- if !@deploy.nil? and !@cloudobj.mu_name.nil? and !@cloudobj.mu_name.empty?
763
- describe # XXX is this actually safe here?
764
- @deploy.addKitten(self.class.cfg_name, @config['name'], self)
765
- elsif !@deploy.nil?
766
- MU.log "#{self} didn't generate a mu_name after being loaded/initialized, dependencies on this resource will probably be confused!", MU::ERR
1093
+ if ["Server", "ServerPool"].include?(self.class.shortname) and @deploy
1094
+ @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: @config.has_key?("basis"))
1095
+ if self.class.shortname == "Server"
1096
+ @groomer = MU::Groomer.new(self)
1097
+ end
1098
+
1099
+ @groomclass = MU::Groomer.loadGroomer(@config["groomer"])
1100
+
1101
+ if windows? or @config['active_directory'] and !@mu_windows_name
1102
+ if !@deploydata.nil? and !@deploydata['mu_windows_name'].nil?
1103
+ @mu_windows_name = @deploydata['mu_windows_name']
1104
+ else
1105
+ # Use the same random differentiator as the "real" name if we're
1106
+ # from a ServerPool. Helpful for admin sanity.
1107
+ unq = @mu_name.sub(/^.*?-(...)$/, '\1')
1108
+ if @config['basis'] and !unq.nil? and !unq.empty?
1109
+ @mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true, use_unique_string: unq, reuse_unique_string: true)
1110
+ else
1111
+ @mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true)
1112
+ end
1113
+ end
1114
+ end
1115
+ class << self
1116
+ attr_reader :groomer
1117
+ attr_reader :groomerclass
1118
+ attr_accessor :mu_windows_name # XXX might be ok as reader now
1119
+ end
1120
+ end
767
1121
  end
768
1122
 
769
1123
  end
770
1124
 
1125
+ def cloud
1126
+ if @cloud
1127
+ @cloud
1128
+ elsif @config and @config['cloud']
1129
+ @config['cloud']
1130
+ elsif self.class.name.match(/^MU::Cloud::([^:]+)::.+/)
1131
+ cloudclass_name = Regexp.last_match[1]
1132
+ if MU::Cloud.supportedClouds.include?(cloudclass_name)
1133
+ cloudclass_name
1134
+ else
1135
+ nil
1136
+ end
1137
+ else
1138
+ nil
1139
+ end
1140
+ end
1141
+
1142
+
771
1143
  # Remove all metadata and cloud resources associated with this object
772
1144
  def destroy
773
1145
  if !@cloudobj.nil? and !@cloudobj.groomer.nil?
@@ -796,23 +1168,99 @@ module MU
796
1168
  end
797
1169
  end
798
1170
  end
1171
+
1172
+ # Return the cloud object's idea of where it lives (project, account,
1173
+ # etc) in the form of an identifier. If not applicable for this object,
1174
+ # we expect to return +nil+.
1175
+ # @return [String,nil]
1176
+ def habitat(nolookup: true)
1177
+ return nil if ["folder", "habitat"].include?(self.class.cfg_name)
1178
+ if @cloudobj
1179
+ @cloudparentclass.habitat(@cloudobj, nolookup: nolookup, deploy: @deploy)
1180
+ else
1181
+ @cloudparentclass.habitat(self, nolookup: nolookup, deploy: @deploy)
1182
+ end
1183
+ end
1184
+
1185
+ def habitat_id(nolookup: false)
1186
+ @habitat_id ||= habitat(nolookup: nolookup)
1187
+ @habitat_id
1188
+ end
1189
+
1190
+ # We're fundamentally a wrapper class, so go ahead and reroute requests
1191
+ # that are meant for our wrapped object.
1192
+ def method_missing(method_sym, *arguments)
1193
+ if @cloudobj
1194
+ MU.log "INVOKING #{method_sym.to_s} FROM PARENT CLOUD OBJECT #{self}", MU::DEBUG, details: arguments
1195
+ @cloudobj.method(method_sym).call(*arguments)
1196
+ else
1197
+ raise NoMethodError, "No such instance method #{method_sym.to_s} available on #{self.class.name}"
1198
+ end
1199
+ end
1200
+
1201
+ # Merge the passed hash into the existing configuration hash of this
1202
+ # cloud object. Currently this is only used by the {MU::Adoption}
1203
+ # module. I don't love exposing this to the whole internal API, but I'm
1204
+ # probably overthinking that.
1205
+ # @param newcfg [Hash]
1206
+ def config!(newcfg)
1207
+ @config.merge!(newcfg)
1208
+ end
799
1209
 
800
- def cloud_desc()
1210
+ def cloud_desc(use_cache: true)
801
1211
  describe
1212
+
802
1213
  if !@cloudobj.nil?
803
- @cloud_desc_cache ||= @cloudobj.cloud_desc
804
- @url = @cloudobj.url if @cloudobj.respond_to?(:url)
1214
+ if @cloudobj.class.instance_methods(false).include?(:cloud_desc)
1215
+ @cloud_desc_cache ||= @cloudobj.cloud_desc
1216
+ end
805
1217
  end
806
- if !@config.nil? and !@cloud_id.nil? and @cloud_desc_cache.nil?
1218
+ if !@config.nil? and !@cloud_id.nil? and (!use_cache or @cloud_desc_cache.nil?)
807
1219
  # The find() method should be returning a Hash with the cloud_id
808
1220
  # as a key and a cloud platform descriptor as the value.
809
1221
  begin
1222
+ args = {
1223
+ :region => @config['region'],
1224
+ :cloud => @config['cloud'],
1225
+ :cloud_id => @cloud_id,
1226
+ :credentials => @credentials,
1227
+ :project => habitat_id, # XXX this belongs in our required_instance_methods hack
1228
+ :flags => @config
1229
+ }
1230
+ @cloudparentclass.required_instance_methods.each { |m|
1231
+ # if respond_to?(m)
1232
+ # args[m] = method(m).call
1233
+ # else
1234
+ args[m] = instance_variable_get(("@"+m.to_s).to_sym)
1235
+ # end
1236
+ }
810
1237
 
811
- matches = self.class.find(region: @config['region'], cloud_id: @cloud_id, flags: @config, credentials: @credentials)
812
- if !matches.nil? and matches.is_a?(Hash) and matches.has_key?(@cloud_id)
813
- @cloud_desc_cache = matches[@cloud_id]
814
- else
815
- MU.log "Failed to find a live #{self.class.shortname} with identifier #{@cloud_id} in #{@credentials}/#{@config['region']}, which has a record in deploy #{@deploy.deploy_id}", MU::WARN, details: caller
1238
+ matches = self.class.find(args)
1239
+ if !matches.nil? and matches.is_a?(Hash)
1240
+ # XXX or if the hash is keyed with an ::Id element, oh boy
1241
+ # puts matches[@cloud_id][:self_link]
1242
+ # puts matches[@cloud_id][:url]
1243
+ # if matches[@cloud_id][:self_link]
1244
+ # @url ||= matches[@cloud_id][:self_link]
1245
+ # elsif matches[@cloud_id][:url]
1246
+ # @url ||= matches[@cloud_id][:url]
1247
+ # elsif matches[@cloud_id][:arn]
1248
+ # @arn ||= matches[@cloud_id][:arn]
1249
+ # end
1250
+ if matches[@cloud_id]
1251
+ @cloud_desc_cache = matches[@cloud_id]
1252
+ else
1253
+ matches.each_pair { |k, v| # flatten out ::Id objects just in case
1254
+ if @cloud_id.to_s == k.to_s
1255
+ @cloud_desc_cache = v
1256
+ break
1257
+ end
1258
+ }
1259
+ end
1260
+ end
1261
+
1262
+ if !@cloud_desc_cache
1263
+ MU.log "cloud_desc via #{self.class.name}.find() failed to locate a live object.\nWas called by #{caller[0]}", MU::WARN, details: args
816
1264
  end
817
1265
  rescue Exception => e
818
1266
  MU.log "Got #{e.inspect} trying to find cloud handle for #{self.class.shortname} #{@mu_name} (#{@cloud_id})", MU::WARN
@@ -858,19 +1306,6 @@ module MU
858
1306
  end
859
1307
  if @deploydata.has_key?('cloud_id')
860
1308
  @cloud_id ||= @deploydata['cloud_id']
861
- else
862
- # XXX temp hack to catch old Amazon-style identifiers. Remove this
863
- # before supporting any other cloud layers, otherwise name
864
- # collision is possible.
865
- ["group_id", "instance_id", "awsname", "identifier", "vpc_id", "id"].each { |identifier|
866
- if @deploydata.has_key?(identifier)
867
- @cloud_id ||= @deploydata[identifier]
868
- if @mu_name.nil? and (identifier == "awsname" or identifier == "identifier" or identifier == "group_id")
869
- @mu_name = @deploydata[identifier]
870
- end
871
- break
872
- end
873
- }
874
1309
  end
875
1310
  end
876
1311
 
@@ -886,7 +1321,7 @@ module MU
886
1321
  # resources in this deployment), as well as for certain config stanzas
887
1322
  # which can refer to external resources (@vpc, @loadbalancers,
888
1323
  # @add_firewall_rules)
889
- def dependencies(use_cache: false)
1324
+ def dependencies(use_cache: false, debug: false)
890
1325
  @dependencies = {} if @dependencies.nil?
891
1326
  @loadbalancers = [] if @loadbalancers.nil?
892
1327
  if @config.nil?
@@ -897,6 +1332,8 @@ module MU
897
1332
  end
898
1333
  @config['dependencies'] = [] if @config['dependencies'].nil?
899
1334
 
1335
+ loglevel = debug ? MU::NOTICE : MU::DEBUG
1336
+
900
1337
  # First, general dependencies. These should all be fellow members of
901
1338
  # the current deployment.
902
1339
  @config['dependencies'].each { |dep|
@@ -904,7 +1341,7 @@ module MU
904
1341
  next if @dependencies[dep['type']].has_key?(dep['name'])
905
1342
  handle = @deploy.findLitterMate(type: dep['type'], name: dep['name']) if !@deploy.nil?
906
1343
  if !handle.nil?
907
- MU.log "Loaded dependency for #{self}: #{dep['name']} => #{handle}", MU::DEBUG
1344
+ MU.log "Loaded dependency for #{self}: #{dep['name']} => #{handle}", loglevel
908
1345
  @dependencies[dep['type']][dep['name']] = handle
909
1346
  else
910
1347
  # XXX yell under circumstances where we should expect to have
@@ -914,62 +1351,100 @@ module MU
914
1351
 
915
1352
  # Special dependencies: my containing VPC
916
1353
  if self.class.can_live_in_vpc and !@config['vpc'].nil?
917
- MU.log "Loading VPC for #{self}", MU::DEBUG, details: @config['vpc']
918
- if !@config['vpc']["vpc_name"].nil? and @deploy
919
- sib_by_name = @deploy.findLitterMate(name: @config['vpc']['vpc_name'], type: "vpcs", return_all: true)
1354
+ # If something hash-ified a MU::Config::Ref here, fix it
1355
+ if !@config['vpc']["id"].nil? and @config['vpc']["id"].is_a?(Hash)
1356
+ @config['vpc']["id"] = MU::Config::Ref.new(@config['vpc']["id"])
1357
+ end
1358
+ if !@config['vpc']["id"].nil? and @config['vpc']["id"].is_a?(MU::Config::Ref) and !@config['vpc']["id"].kitten.nil?
1359
+ @vpc = @config['vpc']["id"].kitten
1360
+ elsif !@config['vpc']["name"].nil? and @deploy
1361
+ MU.log "Attempting findLitterMate on VPC for #{self}", loglevel, details: @config['vpc']
1362
+
1363
+ sib_by_name = @deploy.findLitterMate(name: @config['vpc']['name'], type: "vpcs", return_all: true, habitat: @config['vpc']['project'], debug: debug)
920
1364
  if sib_by_name.is_a?(Array)
921
1365
  if sib_by_name.size == 1
922
1366
  @vpc = matches.first
1367
+ MU.log "Single VPC match for #{self}", loglevel, details: @vpc.to_s
923
1368
  else
924
1369
  # XXX ok but this is the wrong place for this really the config parser needs to sort this out somehow
925
1370
  # we got multiple matches, try to pick one by preferred subnet
926
1371
  # behavior
1372
+ MU.log "Sorting a bunch of VPC matches for #{self}", loglevel, details: sib_by_name.map { |s| s.to_s }.join(", ")
927
1373
  sib_by_name.each { |sibling|
928
1374
  all_private = sibling.subnets.map { |s| s.private? }.all?(true)
929
1375
  all_public = sibling.subnets.map { |s| s.private? }.all?(false)
1376
+ names = sibling.subnets.map { |s| s.name }
1377
+ ids = sibling.subnets.map { |s| s.cloud_id }
930
1378
  if all_private and ["private", "all_private"].include?(@config['vpc']['subnet_pref'])
931
1379
  @vpc = sibling
932
1380
  break
933
1381
  elsif all_public and ["public", "all_public"].include?(@config['vpc']['subnet_pref'])
934
1382
  @vpc = sibling
935
1383
  break
936
- else
937
- MU.log "Got multiple matching VPCs for #{@mu_name}, so I'm arbitrarily choosing #{sibling.mu_name}"
1384
+ elsif @config['vpc']['subnet_name'] and
1385
+ names.include?(@config['vpc']['subnet_name'])
1386
+ puts "CHOOSING #{@vpc.to_s} 'cause it has #{@config['vpc']['subnet_name']}"
1387
+ @vpc = sibling
1388
+ break
1389
+ elsif @config['vpc']['subnet_id'] and
1390
+ ids.include?(@config['vpc']['subnet_id'])
938
1391
  @vpc = sibling
939
1392
  break
940
1393
  end
941
1394
  }
1395
+ if !@vpc
1396
+ sibling = sib_by_name.sample
1397
+ MU.log "Got multiple matching VPCs for #{self.class.cfg_name} #{@mu_name}, so I'm arbitrarily choosing #{sibling.mu_name}", MU::WARN, details: @config['vpc']
1398
+ @vpc = sibling
1399
+ end
942
1400
  end
943
1401
  else
944
1402
  @vpc = sib_by_name
1403
+ MU.log "Found exact VPC match for #{self}", loglevel, details: sib_by_name.to_s
945
1404
  end
1405
+ else
1406
+ MU.log "No shortcuts available to fetch VPC for #{self}", loglevel, details: @config['vpc']
946
1407
  end
947
1408
 
948
- if !@vpc and !@config['vpc']["vpc_name"].nil? and
1409
+ if !@vpc and !@config['vpc']["name"].nil? and
949
1410
  @dependencies.has_key?("vpc") and
950
- @dependencies["vpc"].has_key?(@config['vpc']["vpc_name"])
951
- @vpc = @dependencies["vpc"][@config['vpc']["vpc_name"]]
1411
+ @dependencies["vpc"].has_key?(@config['vpc']["name"])
1412
+ MU.log "Grabbing VPC I see in @dependencies['vpc']['#{@config['vpc']["name"]}'] for #{self}", loglevel, details: @config['vpc']
1413
+ @vpc = @dependencies["vpc"][@config['vpc']["name"]]
952
1414
  elsif !@vpc
953
1415
  tag_key, tag_value = @config['vpc']['tag'].split(/=/, 2) if !@config['vpc']['tag'].nil?
954
- if !@config['vpc'].has_key?("vpc_id") and
1416
+ if !@config['vpc'].has_key?("id") and
955
1417
  !@config['vpc'].has_key?("deploy_id") and !@deploy.nil?
956
1418
  @config['vpc']["deploy_id"] = @deploy.deploy_id
957
1419
  end
1420
+ MU.log "Doing findStray for VPC for #{self}", loglevel, details: @config['vpc']
958
1421
  vpcs = MU::MommaCat.findStray(
959
1422
  @config['cloud'],
960
1423
  "vpc",
961
1424
  deploy_id: @config['vpc']["deploy_id"],
962
- cloud_id: @config['vpc']["vpc_id"],
963
- name: @config['vpc']["vpc_name"],
1425
+ cloud_id: @config['vpc']["id"],
1426
+ name: @config['vpc']["name"],
964
1427
  tag_key: tag_key,
965
1428
  tag_value: tag_value,
1429
+ habitats: [@project_id],
966
1430
  region: @config['vpc']["region"],
967
1431
  calling_deploy: @deploy,
968
- dummy_ok: true
1432
+ credentials: @credentials,
1433
+ dummy_ok: true,
1434
+ debug: debug
969
1435
  )
970
1436
  @vpc = vpcs.first if !vpcs.nil? and vpcs.size > 0
971
1437
  end
972
- if !@vpc.nil? and (
1438
+ if @vpc and @vpc.config and @vpc.config['bastion'] and
1439
+ @vpc.config['bastion'].to_h['name'] != @config['name']
1440
+ refhash = @vpc.config['bastion'].to_h
1441
+ refhash['deploy_id'] ||= @vpc.deploy.deploy_id
1442
+ natref = MU::Config::Ref.get(refhash)
1443
+ if natref and natref.kitten(@vpc.deploy)
1444
+ @nat = natref.kitten(@vpc.deploy)
1445
+ end
1446
+ end
1447
+ if @nat.nil? and !@vpc.nil? and (
973
1448
  @config['vpc'].has_key?("nat_host_id") or
974
1449
  @config['vpc'].has_key?("nat_host_tag") or
975
1450
  @config['vpc'].has_key?("nat_host_ip") or
@@ -1008,6 +1483,22 @@ module MU
1008
1483
  @vpc = self
1009
1484
  end
1010
1485
 
1486
+ # Google accounts usually have a useful default VPC we can use
1487
+ if @vpc.nil? and @project_id and @cloud == "Google" and
1488
+ self.class.can_live_in_vpc
1489
+ MU.log "Seeing about default VPC for #{self.to_s}", MU::NOTICE
1490
+ vpcs = MU::MommaCat.findStray(
1491
+ "Google",
1492
+ "vpc",
1493
+ cloud_id: "default",
1494
+ habitats: [@project_id],
1495
+ credentials: @credentials,
1496
+ dummy_ok: true,
1497
+ debug: debug
1498
+ )
1499
+ @vpc = vpcs.first if !vpcs.nil? and vpcs.size > 0
1500
+ end
1501
+
1011
1502
  # Special dependencies: LoadBalancers I've asked to attach to an
1012
1503
  # instance.
1013
1504
  if @config.has_key?("loadbalancers")
@@ -1046,17 +1537,46 @@ module MU
1046
1537
  MU::Cloud::ALPHA
1047
1538
  end
1048
1539
 
1540
+ # Return a list of "container" artifacts, by class, that apply to this
1541
+ # resource type in a cloud provider. This is so methods that call find
1542
+ # know whether to call +find+ with identifiers for parent resources.
1543
+ # This is similar in purpose to the +isGlobal?+ resource class method,
1544
+ # which tells our search functions whether or not a resource scopes to
1545
+ # a region. In almost all cases this is one-entry list consisting of
1546
+ # +:Habitat+. Notable exceptions include most implementations of
1547
+ # +Habitat+, which either reside inside a +:Folder+ or nothing at all;
1548
+ # whereas a +:Folder+ tends to not have any containing parent. Very few
1549
+ # resource implementations will need to override this.
1550
+ # A +nil+ entry in this list is interpreted as "this resource can be
1551
+ # global."
1552
+ # @return [Array<Symbol,nil>]
1553
+ def self.canLiveIn
1554
+ if self.shortname == "Folder"
1555
+ [nil, :Folder]
1556
+ elsif self.shortname == "Habitat"
1557
+ [:Folder]
1558
+ else
1559
+ [:Habitat]
1560
+ end
1561
+ end
1562
+
1049
1563
  def self.find(*flags)
1050
1564
  allfound = {}
1051
1565
 
1052
- MU::Cloud.supportedClouds.each { |cloud|
1566
+ MU::Cloud.availableClouds.each { |cloud|
1053
1567
  begin
1054
1568
  args = flags.first
1569
+ next if args[:cloud] and args[:cloud] != cloud
1055
1570
  # skip this cloud if we have a region argument that makes no
1056
1571
  # sense there
1057
1572
  cloudbase = Object.const_get("MU").const_get("Cloud").const_get(cloud)
1573
+ next if cloudbase.listCredentials.nil? or cloudbase.listCredentials.empty? or cloudbase.credConfig(args[:credentials]).nil?
1058
1574
  if args[:region] and cloudbase.respond_to?(:listRegions)
1059
- next if !cloudbase.listRegions(credentials: args[:credentials]).include?(args[:region])
1575
+ if !cloudbase.listRegions(credentials: args[:credentials])
1576
+ MU.log "Failed to get region list for credentials #{args[:credentials]} in cloud #{cloud}", MU::ERR, details: caller
1577
+ else
1578
+ next if !cloudbase.listRegions(credentials: args[:credentials]).include?(args[:region])
1579
+ end
1060
1580
  end
1061
1581
  begin
1062
1582
  cloudclass = MU::Cloud.loadCloudType(cloud, shortname)
@@ -1094,9 +1614,9 @@ module MU
1094
1614
  end
1095
1615
  end
1096
1616
 
1097
- if shortname == "Server"
1617
+ if shortname == "Server" or shortname == "ServerPool"
1098
1618
  def windows?
1099
- return true if %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 windows}.include?(@config['platform'])
1619
+ return true if %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 win2k19 windows}.include?(@config['platform'])
1100
1620
  begin
1101
1621
  return true if cloud_desc.respond_to?(:platform) and cloud_desc.platform == "Windows"
1102
1622
  # XXX ^ that's AWS-speak, doesn't cover GCP or anything else; maybe we should require cloud layers to implement this so we can just call @cloudobj.windows?
@@ -1330,7 +1850,9 @@ module MU
1330
1850
  if !output.nil? and !output.empty?
1331
1851
  raise MU::Cloud::BootstrapTempFail, "Linux package manager is still doing something, need to wait (#{output})"
1332
1852
  end
1333
- if !@config['skipinitialupdates']
1853
+ if !@config['skipinitialupdates'] and
1854
+ !@config['scrub_mu_isms'] and
1855
+ !@config['userdata_script']
1334
1856
  output = ssh.exec!(lnx_updates_check)
1335
1857
  if !output.nil? and output.match(/userdata still running/)
1336
1858
  raise MU::Cloud::BootstrapTempFail, "Waiting for initial userdata system updates to complete"
@@ -1464,7 +1986,7 @@ module MU
1464
1986
 
1465
1987
  if retries < max_retries
1466
1988
  retries = retries + 1
1467
- msg = "ssh #{ssh_user}@#{@config['mu_name']}: #{e.message}, waiting #{retry_interval}s (attempt #{retries}/#{max_retries})", MU::WARN
1989
+ msg = "ssh #{ssh_user}@#{@mu_name}: #{e.message}, waiting #{retry_interval}s (attempt #{retries}/#{max_retries})", MU::WARN
1468
1990
  if retries == 1 or (retries/max_retries <= 0.5 and (retries % 3) == 0)
1469
1991
  MU.log msg, MU::NOTICE
1470
1992
  elsif retries/max_retries > 0.5
@@ -1473,7 +1995,7 @@ module MU
1473
1995
  sleep retry_interval
1474
1996
  retry
1475
1997
  else
1476
- raise MuError, "#{@config['mu_name']}: #{e.inspect} trying to connect with SSH, max_retries exceeded", e.backtrace
1998
+ raise MuError, "#{@mu_name}: #{e.inspect} trying to connect with SSH, max_retries exceeded", e.backtrace
1477
1999
  end
1478
2000
  end
1479
2001
  return session
@@ -1501,6 +2023,17 @@ module MU
1501
2023
  MU::MommaCat.unlockAll
1502
2024
  end
1503
2025
 
2026
+ # A hook that is always called just before each instance method is
2027
+ # invoked, so that we can ensure that repetitive setup tasks (like
2028
+ # resolving +:resource_group+ for Azure resources) have always been
2029
+ # done.
2030
+ def resourceInitHook
2031
+ @cloud ||= cloud
2032
+ if @cloudparentclass.respond_to?(:resourceInitHook)
2033
+ @cloudparentclass.resourceInitHook(@cloudobj, @deploy)
2034
+ end
2035
+ end
2036
+
1504
2037
  # Wrap the instance methods that this cloud resource type has to
1505
2038
  # implement.
1506
2039
  MU::Cloud.resource_types[name.to_sym][:instance].each { |method|
@@ -1538,15 +2071,19 @@ module MU
1538
2071
  if (method == :create or method == :groom or method == :postBoot) and
1539
2072
  (!@destroyed and !@cloudobj.destroyed)
1540
2073
  deploydata = @cloudobj.method(:notify).call
2074
+ @deploydata ||= deploydata # XXX I don't remember why we're not just doing this from the get-go; maybe because we prefer some mangling occurring in @deploy.notify?
1541
2075
  if deploydata.nil? or !deploydata.is_a?(Hash)
1542
- MU.log "#{self} notify method did not return a Hash of deployment data", MU::WARN
2076
+ MU.log "#{self} notify method did not return a Hash of deployment data, attempting to fill in with cloud descriptor #{@cloudobj.cloud_id}", MU::WARN
1543
2077
  deploydata = MU.structToHash(@cloudobj.cloud_desc)
2078
+ raise MuError, "Failed to collect metadata about #{self}" if deploydata.nil?
1544
2079
  end
1545
- deploydata['cloud_id'] = @cloudobj.cloud_id if !@cloudobj.cloud_id.nil?
2080
+ deploydata['cloud_id'] ||= @cloudobj.cloud_id if !@cloudobj.cloud_id.nil?
1546
2081
  deploydata['mu_name'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil?
2082
+ deploydata['nodename'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil?
2083
+ deploydata.delete("#MUOBJECT")
1547
2084
  @deploy.notify(self.class.cfg_plural, @config['name'], deploydata, triggering_node: @cloudobj, delayed_save: @delayed_save) if !@deploy.nil?
1548
2085
  elsif method == :notify
1549
- retval['cloud_id'] = @cloudobj.cloud_id if !@cloudobj.cloud_id.nil?
2086
+ retval['cloud_id'] = @cloudobj.cloud_id.to_s if !@cloudobj.cloud_id.nil?
1550
2087
  retval['mu_name'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil?
1551
2088
  @deploy.notify(self.class.cfg_plural, @config['name'], retval, triggering_node: @cloudobj, delayed_save: @delayed_save) if !@deploy.nil?
1552
2089
  end