cloud-mu 2.1.0beta → 3.0.0beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -0,0 +1,810 @@
1
+ # Copyright:: Copyright (c) 2019 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
+ require 'net/ssh'
16
+ require 'net/ssh/multi'
17
+ require 'net/ssh/proxy/command'
18
+ autoload :OpenStruct, "ostruct"
19
+ autoload :Timeout, "timeout"
20
+ autoload :ERB, "erb"
21
+ autoload :Base64, "base64"
22
+ require 'open-uri'
23
+
24
+ module MU
25
+ class Cloud
26
+ class Azure
27
+ # A server as configured in {MU::Config::BasketofKittens::servers}.
28
+ class Server < MU::Cloud::Server
29
+
30
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
31
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
32
+ def initialize(**args)
33
+ super
34
+
35
+ @userdata = if @config['userdata_script']
36
+ @config['userdata_script']
37
+ elsif @deploy and !@scrub_mu_isms
38
+ MU::Cloud.fetchUserdata(
39
+ platform: @config["platform"],
40
+ cloud: "Azure",
41
+ credentials: @config['credentials'],
42
+ template_variables: {
43
+ "deployKey" => Base64.urlsafe_encode64(@deploy.public_key),
44
+ "deploySSHKey" => @deploy.ssh_public_key,
45
+ "muID" => MU.deploy_id,
46
+ "muUser" => MU.mu_user,
47
+ "publicIP" => MU.mu_public_ip,
48
+ "adminBucketName" => MU::Cloud::Azure.adminBucketName(@credentials),
49
+ "chefVersion" => MU.chefVersion,
50
+ "skipApplyUpdates" => @config['skipinitialupdates'],
51
+ "windowsAdminName" => @config['windows_admin_username'],
52
+ "mommaCatPort" => MU.mommaCatPort,
53
+ "resourceName" => @config["name"],
54
+ "resourceType" => "server",
55
+ "platform" => @config["platform"]
56
+ },
57
+ custom_append: @config['userdata_script']
58
+ )
59
+ end
60
+
61
+ if !@mu_name
62
+ if kitten_cfg.has_key?("basis")
63
+ @mu_name = @deploy.getResourceName(@config['name'], need_unique_string: true)
64
+ else
65
+ @mu_name = @deploy.getResourceName(@config['name'])
66
+ end
67
+ end
68
+ @config['instance_secret'] ||= Password.random(50)
69
+
70
+ end
71
+
72
+ # Return the date/time a machine image was created.
73
+ # @param image_id [String]: URL to a Azure disk image
74
+ # @param credentials [String]
75
+ # @return [DateTime]
76
+ def self.imageTimeStamp(image_id, credentials: nil)
77
+ return DateTime.new(0) # Azure doesn't seem to keep this anywhere, boo
78
+ # begin
79
+ # img = fetchImage(image_id, credentials: credentials)
80
+ # return DateTime.new if img.nil?
81
+ # return DateTime.parse(img.creation_timestamp)
82
+ # rescue ::Azure::Apis::ClientError => e
83
+ # end
84
+ #
85
+ # return DateTime.new
86
+ end
87
+
88
+ # Called automatically by {MU::Deploy#createResources}
89
+ def create
90
+ create_update
91
+
92
+ if !@config['async_groom']
93
+ sleep 5
94
+ MU::MommaCat.lock(@cloud_id.to_s+"-create")
95
+ if !postBoot
96
+ MU.log "#{@config['name']} is already being groomed, skipping", MU::NOTICE
97
+ else
98
+ MU.log "Node creation complete for #{@config['name']}"
99
+ end
100
+ MU::MommaCat.unlock(@cloud_id.to_s+"-create")
101
+ end
102
+
103
+ end
104
+
105
+ # Return a BoK-style config hash describing a NAT instance. We use this
106
+ # to approximate NAT gateway functionality with a plain instance.
107
+ # @return [Hash]
108
+ def self.genericNAT
109
+ return {
110
+ "cloud" => "Azure",
111
+ "src_dst_check" => false,
112
+ "bastion" => true,
113
+ "size" => "Standard_B2s",
114
+ "run_list" => [ "mu-utility::nat" ],
115
+ "platform" => "centos7",
116
+ "associate_public_ip" => true,
117
+ "static_ip" => { "assign_ip" => true },
118
+ }
119
+ end
120
+
121
+ # Ask the Azure API to stop this node
122
+ def stop
123
+ MU.log "XXX Stopping #{@cloud_id}"
124
+ end
125
+
126
+ # Ask the Azure API to start this node
127
+ def start
128
+ MU.log "XXX Starting #{@cloud_id}"
129
+ end
130
+
131
+ # Ask the Azure API to restart this node
132
+ # XXX unimplemented
133
+ def reboot(hard = false)
134
+ return if @cloud_id.nil?
135
+
136
+ end
137
+
138
+ # Figure out what's needed to SSH into this server.
139
+ # @return [Array<String>]: nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name, alternate_names
140
+ def getSSHConfig
141
+ node, config, deploydata = describe(cloud_id: @cloud_id)
142
+ # XXX add some awesome alternate names from metadata and make sure they end
143
+ # up in MU::MommaCat's ssh config wangling
144
+ ssh_keydir = Etc.getpwuid(Process.uid).dir+"/.ssh"
145
+ return nil if @config.nil? or @deploy.nil?
146
+
147
+ nat_ssh_key = nat_ssh_user = nat_ssh_host = nil
148
+ if !@config["vpc"].nil? and !MU::Cloud::Azure::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['credentials'])
149
+
150
+ if !@nat.nil? and @nat.mu_name != @mu_name
151
+ if @nat.cloud_desc.nil?
152
+ MU.log "NAT was missing cloud descriptor when called in #{@mu_name}'s getSSHConfig", MU::ERR
153
+ return nil
154
+ end
155
+ foo, bar, baz, nat_ssh_host, nat_ssh_user, nat_ssh_key = @nat.getSSHConfig
156
+ if nat_ssh_user.nil? and !nat_ssh_host.nil?
157
+ MU.log "#{@config["name"]} (#{MU.deploy_id}) is configured to use #{@config['vpc']} NAT #{nat_ssh_host}, but username isn't specified. Guessing root.", MU::ERR, details: caller
158
+ nat_ssh_user = "root"
159
+ end
160
+ end
161
+ end
162
+
163
+ if @config['ssh_user'].nil?
164
+ if windows?
165
+ @config['ssh_user'] = "Administrator"
166
+ else
167
+ @config['ssh_user'] = "root"
168
+ end
169
+ end
170
+
171
+ return [nat_ssh_key, nat_ssh_user, nat_ssh_host, canonicalIP, @config['ssh_user'], @deploy.ssh_key_name]
172
+
173
+ end
174
+
175
+ # Apply tags, bootstrap our configuration management, and other
176
+ # administravia for a new instance.
177
+ def postBoot(instance_id = nil)
178
+ if !instance_id.nil?
179
+ @cloud_id ||= instance_id
180
+ end
181
+
182
+ # Unless we're planning on associating a different IP later, set up a
183
+ # DNS entry for this thing and let it sync in the background. We'll
184
+ # come back to it later.
185
+ if @config['static_ip'].nil? && !@named
186
+ MU::MommaCat.nameKitten(self)
187
+ @named = true
188
+ end
189
+
190
+ nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name = getSSHConfig
191
+ if !nat_ssh_host and !MU::Cloud::Azure::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['credentials'])
192
+ # XXX check if canonical_ip is in the private ranges
193
+ # raise MuError, "#{node} has no NAT host configured, and I have no other route to it"
194
+ end
195
+
196
+ # See if this node already exists in our config management. If it does,
197
+ # we're done.
198
+ if @groomer.haveBootstrapped?
199
+ MU.log "Node #{@mu_name} has already been bootstrapped, skipping groomer setup.", MU::NOTICE
200
+ @groomer.saveDeployData
201
+ MU::MommaCat.unlock(@cloud_id.to_s+"-orchestrate")
202
+ MU::MommaCat.unlock(@cloud_id.to_s+"-groom")
203
+ return true
204
+ end
205
+
206
+ @groomer.bootstrap
207
+
208
+ # Make sure we got our name written everywhere applicable
209
+ if !@named
210
+ MU::MommaCat.nameKitten(self)
211
+ @named = true
212
+ end
213
+
214
+ MU::MommaCat.unlock(@cloud_id.to_s+"-groom")
215
+ MU::MommaCat.unlock(@cloud_id.to_s+"-orchestrate")
216
+ return true
217
+ end #postBoot
218
+
219
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching instances
220
+ def self.find(**args)
221
+ found = {}
222
+ # told one, we may have to search all the ones we can see.
223
+ resource_groups = if args[:resource_group]
224
+ [args[:resource_group]]
225
+ elsif args[:cloud_id] and args[:cloud_id].is_a?(MU::Cloud::Azure::Id)
226
+ [args[:cloud_id].resource_group]
227
+ else
228
+ MU::Cloud::Azure.resources(credentials: args[:credentials]).resource_groups.list.map { |rg| rg.name }
229
+ end
230
+
231
+ if args[:cloud_id]
232
+ id_str = args[:cloud_id].is_a?(MU::Cloud::Azure::Id) ? args[:cloud_id].name : args[:cloud_id]
233
+ resource_groups.each { |rg|
234
+ begin
235
+ resp = MU::Cloud::Azure.compute(credentials: args[:credentials]).virtual_machines.get(rg, id_str)
236
+ next if resp.nil?
237
+ found[Id.new(resp.id)] = resp
238
+ rescue MU::Cloud::Azure::APIError => e
239
+ # this is fine, we're doing a blind search after all
240
+ end
241
+ }
242
+ else
243
+ if args[:resource_group]
244
+ MU::Cloud::Azure.compute(credentials: args[:credentials]).virtual_machines.list(args[:resource_group]).each { |vm|
245
+ found[Id.new(vm.id)] = vm
246
+ }
247
+ else
248
+ MU::Cloud::Azure.compute(credentials: args[:credentials]).virtual_machines.list_all.each { |vm|
249
+ found[Id.new(vm.id)] = vm
250
+ }
251
+ end
252
+ end
253
+
254
+ found
255
+ end
256
+
257
+ # Return a description of this resource appropriate for deployment
258
+ # metadata. Arguments reflect the return values of the MU::Cloud::[Resource].describe method
259
+ def notify
260
+ MU.structToHash(cloud_desc)
261
+ end
262
+
263
+ # Called automatically by {MU::Deploy#createResources}
264
+ def groom
265
+ create_update
266
+
267
+ MU::MommaCat.lock(@cloud_id.to_s+"-groom")
268
+
269
+ node, config, deploydata = describe(cloud_id: @cloud_id)
270
+
271
+ if node.nil? or node.empty?
272
+ raise MuError, "MU::Cloud::Azure::Server.groom was called without a mu_name"
273
+ end
274
+
275
+ # Make double sure we don't lose a cached mu_windows_name value.
276
+ if windows? or !@config['active_directory'].nil?
277
+ if @mu_windows_name.nil?
278
+ @mu_windows_name = deploydata['mu_windows_name']
279
+ end
280
+ end
281
+
282
+ @groomer.saveDeployData
283
+
284
+ begin
285
+ @groomer.run(purpose: "Full Initial Run", max_retries: 15)
286
+ rescue MU::Groomer::RunError
287
+ MU.log "Proceeding after failed initial Groomer run, but #{node} may not behave as expected!", MU::WARN
288
+ end
289
+
290
+ if !@config['create_image'].nil? and !@config['image_created']
291
+ img_cfg = @config['create_image']
292
+ # Scrub things that don't belong on an AMI
293
+ session = getSSHSession
294
+ sudo = purgecmd = ""
295
+ sudo = "sudo" if @config['ssh_user'] != "root"
296
+ if windows?
297
+ purgecmd = "rm -rf /cygdrive/c/mu_installed_chef"
298
+ else
299
+ purgecmd = "rm -rf /opt/mu_installed_chef"
300
+ end
301
+ if img_cfg['image_then_destroy']
302
+ if windows?
303
+ purgecmd = "rm -rf /cygdrive/c/chef/ /home/#{@config['windows_admin_username']}/.ssh/authorized_keys /home/Administrator/.ssh/authorized_keys /cygdrive/c/mu-installer-ran-updates /cygdrive/c/mu_installed_chef"
304
+ # session.exec!("powershell -Command \"& {(Get-WmiObject -Class Win32_Product -Filter \"Name='UniversalForwarder'\").Uninstall()}\"")
305
+ else
306
+ purgecmd = "#{sudo} rm -rf /root/.ssh/authorized_keys /etc/ssh/ssh_host_*key* /etc/chef /etc/opscode/* /.mu-installer-ran-updates /var/chef /opt/mu_installed_chef /opt/chef ; #{sudo} sed -i 's/^HOSTNAME=.*//' /etc/sysconfig/network"
307
+ end
308
+ end
309
+ session.exec!(purgecmd)
310
+ session.close
311
+ stop
312
+ image_id = MU::Cloud::Azure::Server.createImage(
313
+ name: MU::Cloud::Azure.nameStr(@mu_name),
314
+ instance_id: @cloud_id,
315
+ region: @config['region'],
316
+ storage: @config['storage'],
317
+ family: ("mu-"+@config['platform']+"-"+MU.environment).downcase,
318
+ project: @project_id,
319
+ exclude_storage: img_cfg['image_exclude_storage'],
320
+ make_public: img_cfg['public'],
321
+ tags: @config['tags'],
322
+ zone: @config['availability_zone'],
323
+ credentials: @config['credentials']
324
+ )
325
+ @deploy.notify("images", @config['name'], {"image_id" => image_id})
326
+ @config['image_created'] = true
327
+ if img_cfg['image_then_destroy']
328
+ MU.log "Image #{image_id} ready, removing source node #{node}"
329
+ MU::Cloud::Azure.compute(credentials: @config['credentials']).delete_instance(
330
+ @project_id,
331
+ @config['availability_zone'],
332
+ @cloud_id
333
+ )
334
+ destroy
335
+ else
336
+ start
337
+ end
338
+ end
339
+
340
+ MU::MommaCat.unlock(@cloud_id.to_s+"-groom")
341
+ end
342
+
343
+ # Create an image out of a running server. Requires either the name of a MU resource in the current deployment, or the cloud provider id of a running instance.
344
+ # @param name [String]: The MU resource name of the server to use as the basis for this image.
345
+ # @param instance_id [String]: The cloud provider resource identifier of the server to use as the basis for this image.
346
+ # @param storage [Hash]: The storage devices to include in this image.
347
+ # @param exclude_storage [Boolean]: Do not include the storage device profile of the running instance when creating this image.
348
+ # @param region [String]: The cloud provider region
349
+ # @param tags [Array<String>]: Extra/override tags to apply to the image.
350
+ # @return [String]: The cloud provider identifier of the new machine image.
351
+ def self.createImage(name: nil, instance_id: nil, storage: {}, exclude_storage: false, project: nil, make_public: false, tags: [], region: nil, family: "mu", zone: MU::Cloud::Azure.listAZs.sample, credentials: nil)
352
+ end
353
+
354
+ # Return the IP address that we, the Mu server, should be using to access
355
+ # this host via the network. Note that this does not factor in SSH
356
+ # bastion hosts that may be in the path, see getSSHConfig if that's what
357
+ # you need.
358
+ def canonicalIP
359
+ mu_name, config, deploydata = describe(cloud_id: @cloud_id)
360
+
361
+ if !cloud_desc
362
+ raise MuError, "Couldn't retrieve cloud descriptor for server #{self}"
363
+ end
364
+
365
+ private_ips = []
366
+ public_ips = []
367
+
368
+ cloud_desc.network_profile.network_interfaces.each { |iface|
369
+ iface_id = Id.new(iface.is_a?(Hash) ? iface['id'] : iface.id)
370
+ iface_desc = MU::Cloud::Azure.network(credentials: @credentials).network_interfaces.get(@resource_group, iface_id.to_s)
371
+ iface_desc.ip_configurations.each { |ipcfg|
372
+ private_ips << ipcfg.private_ipaddress
373
+ if ipcfg.respond_to?(:public_ipaddress) and ipcfg.public_ipaddress
374
+ ip_id = Id.new(ipcfg.public_ipaddress.id)
375
+ ip_desc = MU::Cloud::Azure.network(credentials: @credentials).public_ipaddresses.get(@resource_group, ip_id.to_s)
376
+ if ip_desc
377
+ public_ips << ip_desc.ip_address
378
+ end
379
+ end
380
+ }
381
+ }
382
+
383
+ # Our deploydata gets corrupted often with server pools, this will cause us to use the wrong IP to identify a node
384
+ # which will cause us to create certificates, DNS records and other artifacts with incorrect information which will cause our deploy to fail.
385
+ # The cloud_id is always correct so lets use 'cloud_desc' to get the correct IPs
386
+ if MU::Cloud::Azure::VPC.haveRouteToInstance?(cloud_desc, credentials: @config['credentials']) or public_ips.size == 0
387
+ @config['canonical_ip'] = private_ips.first
388
+ return private_ips.first
389
+ else
390
+ @config['canonical_ip'] = public_ips.first
391
+ return public_ips.first
392
+ end
393
+ end
394
+
395
+ # return [String]: A password string.
396
+ def getWindowsAdminPassword
397
+ end
398
+
399
+ # Add a volume to this instance
400
+ # @param dev [String]: Device name to use when attaching to instance
401
+ # @param size [String]: Size (in gb) of the new volume
402
+ # @param type [String]: Cloud storage type of the volume, if applicable
403
+ # @param delete_on_termination [Boolean]: Value of delete_on_termination flag to set
404
+ def addVolume(dev, size, type: "pd-standard", delete_on_termination: false)
405
+ end
406
+
407
+ # Determine whether the node in question exists at the Cloud provider
408
+ # layer.
409
+ # @return [Boolean]
410
+ def active?
411
+ !cloud_desc.nil?
412
+ end
413
+
414
+ # Does this resource type exist as a global (cloud-wide) artifact, or
415
+ # is it localized to a region/zone?
416
+ # @return [Boolean]
417
+ def self.isGlobal?
418
+ false
419
+ end
420
+
421
+ # Denote whether this resource implementation is experiment, ready for
422
+ # testing, or ready for production use.
423
+ def self.quality
424
+ MU::Cloud::BETA
425
+ end
426
+
427
+ # Remove all instances associated with the currently loaded deployment. Also cleans up associated volumes, droppings in the MU master's /etc/hosts and ~/.ssh, and in whatever Groomer was used.
428
+ # @param noop [Boolean]: If true, will only print what would be done
429
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
430
+ # @param region [String]: The cloud provider region
431
+ # @return [void]
432
+ def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
433
+ end
434
+
435
+ # Cloud-specific configuration properties.
436
+ # @param config [MU::Config]: The calling MU::Config object
437
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
438
+ def self.schema(config)
439
+ toplevel_required = []
440
+ hosts_schema = MU::Config::CIDR_PRIMITIVE
441
+ hosts_schema["pattern"] = "^(\\d+\\.\\d+\\.\\d+\\.\\d+\/[0-9]{1,2}|\\*)$"
442
+ schema = {
443
+ "roles" => MU::Cloud::Azure::User.schema(config)[1]["roles"],
444
+ "ingress_rules" => {
445
+ "items" => {
446
+ "properties" => {
447
+ "hosts" => {
448
+ "type" => "array",
449
+ "items" => hosts_schema
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ [toplevel_required, schema]
456
+ end
457
+
458
+ # Confirm that the given instance size is valid for the given region.
459
+ # If someone accidentally specified an equivalent size from some other cloud provider, return something that makes sense. If nothing makes sense, return nil.
460
+ # @param size [String]: Instance type to check
461
+ # @param region [String]: Region to check against
462
+ # @return [String,nil]
463
+ def self.validateInstanceType(size, region)
464
+ size = size.dup.to_s
465
+ types = (MU::Cloud::Azure.listInstanceTypes(region))[region]
466
+ if types and (size.nil? or !types.has_key?(size))
467
+ # See if it's a type we can approximate from one of the other clouds
468
+ foundmatch = false
469
+ MU::Cloud.availableClouds.each { |cloud|
470
+ next if cloud == "Azure"
471
+ cloudbase = Object.const_get("MU").const_get("Cloud").const_get(cloud)
472
+ foreign_types = (cloudbase.listInstanceTypes)[cloudbase.myRegion]
473
+ if foreign_types and foreign_types.size > 0 and foreign_types.has_key?(size)
474
+ vcpu = foreign_types[size]["vcpu"]
475
+ mem = foreign_types[size]["memory"]
476
+ ecu = foreign_types[size]["ecu"]
477
+ types.keys.sort.reverse.each { |type|
478
+ features = types[type]
479
+ next if ecu == "Variable" and ecu != features["ecu"]
480
+ next if features["vcpu"] != vcpu
481
+ if (features["memory"] - mem.to_f).abs < 0.10*mem
482
+ foundmatch = true
483
+ MU.log "You specified #{cloud} instance type '#{size}.' Approximating with Azure Compute type '#{type}.'", MU::WARN
484
+ size = type
485
+ break
486
+ end
487
+ }
488
+ end
489
+ break if foundmatch
490
+ }
491
+
492
+ if !foundmatch
493
+ MU.log "Invalid size '#{size}' for Azure Compute instance in #{region}. Supported types:", MU::ERR, details: types.keys.sort.join(", ")
494
+ return nil
495
+ end
496
+ end
497
+ size
498
+ end
499
+
500
+
501
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::servers}, bare and unvalidated.
502
+ # @param server [Hash]: The resource to process and validate
503
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
504
+ # @return [Boolean]: True if validation succeeded, False otherwise
505
+ def self.validateConfig(server, configurator)
506
+ ok = true
507
+
508
+ server['region'] ||= MU::Cloud::Azure.myRegion(server['credentials'])
509
+ server['ssh_user'] ||= "muadmin"
510
+
511
+ server['size'] = validateInstanceType(server["size"], server["region"])
512
+ if server['image_id'].nil?
513
+ img_id = MU::Cloud.getStockImage("Azure", platform: server['platform'])
514
+ if img_id
515
+ server['image_id'] = configurator.getTail("server"+server['name']+"Image", value: img_id, prettyname: "server"+server['name']+"Image")
516
+ else
517
+ MU.log "No image specified for #{server['name']} and no default available for platform #{server['platform']}", MU::ERR, details: server
518
+ ok = false
519
+ end
520
+ end
521
+
522
+ image_desc = MU::Cloud::Azure::Server.fetchImage(server['image_id'].to_s, credentials: server['credentials'], region: server['region'])
523
+ if image_desc.plan
524
+ terms = MU::Cloud::Azure.marketplace(credentials: @credentials).marketplace_agreements.get(image_desc.plan.publisher, image_desc.plan.product, image_desc.plan.name)
525
+ if !terms.accepted
526
+ MU.log "Deploying #{server['name']} will automatically agree to the licensing terms for #{terms.product}", MU::NOTICE, details: terms.license_text_link
527
+ end
528
+ end
529
+
530
+ if !image_desc
531
+ MU.log "Failed to locate an Azure VM image for #{server['name']} from #{server['image_id']} in #{server['region']}", MU::ERR
532
+ ok = false
533
+ else
534
+ server['image_id'] = image_desc.id
535
+ end
536
+
537
+ if server['add_firewall_rules'] and server['add_firewall_rules'].size == 0
538
+ MU.log "Azure resources can only have one security group per network interface; use ingress_rules instead of add_firewall_rules.", MU::ERR
539
+ ok = false
540
+ end
541
+
542
+ # Azure doesn't have default VPCs, so our fallback approach will be
543
+ # to generate one on the fly.
544
+ if server['vpc'].nil?
545
+ vpc = {
546
+ "name" => server['name']+"vpc",
547
+ "cloud" => "Azure",
548
+ "region" => server['region'],
549
+ "credentials" => server['credentials']
550
+ }
551
+ if !configurator.insertKitten(vpc, "vpcs", true)
552
+ ok = false
553
+ end
554
+ server['dependencies'] ||= []
555
+
556
+ server['dependencies'] << {
557
+ "type" => "vpc",
558
+ "name" => server['name']+"vpc"
559
+ }
560
+ # XXX what happens if there's no natstion here?
561
+ server['dependencies'] << {
562
+ "type" => "server",
563
+ "name" => server['name']+"vpc-natstion",
564
+ "phase" => "groom"
565
+ }
566
+ server['vpc'] = {
567
+ "name" => server['name']+"vpc",
568
+ "subnet_pref" => "private"
569
+ }
570
+ end
571
+ server['vpc']['subnet_pref'] ||= "private"
572
+
573
+ svcacct_desc = {
574
+ "name" => server["name"]+"user",
575
+ "region" => server["region"],
576
+ "type" => "service",
577
+ "cloud" => "Azure",
578
+ "create_api_key" => true,
579
+ "credentials" => server["credentials"],
580
+ "roles" => server["roles"]
581
+ }
582
+ server['dependencies'] ||= []
583
+ server['dependencies'] << {
584
+ "type" => "user",
585
+ "name" => server["name"]+"user"
586
+ }
587
+
588
+ ok = false if !configurator.insertKitten(svcacct_desc, "users")
589
+
590
+ ok
591
+ end
592
+
593
+ def self.diskConfig(config, create = true, disk_as_url = true, credentials: nil)
594
+ end
595
+
596
+ # Retrieve the cloud descriptor for an Azure machine image
597
+ # @param image_id [String]: A full Azure resource id, or a shorthand string like <tt>OpenLogic/CentOS/7.6/7.6.20190808</tt>. The third and fourth fields (major version numbers and release numbers, by convention) can be partial, and the release number can be omitted entirely. We default to the most recent matching release when applicable.
598
+ # @param credentials [String]
599
+ # @return [Azure::Compute::Mgmt::V2019_03_01::Models::VirtualMachineImage]
600
+ def self.fetchImage(image_id, credentials: nil, region: MU::Cloud::Azure.myRegion)
601
+
602
+ publisher = offer = sku = version = nil
603
+ if image_id.match(/\/Subscriptions\/[^\/]+\/Providers\/Microsoft.Compute\/Locations\/([^\/]+)\/Publishers\/([^\/]+)\/ArtifactTypes\/VMImage\/Offers\/([^\/]+)\/Skus\/([^\/]+)\/Versions\/([^\/]+)$/)
604
+ region = Regexp.last_match[1]
605
+ publisher = Regexp.last_match[2]
606
+ offer = Regexp.last_match[3]
607
+ sku = Regexp.last_match[4]
608
+ version = Regexp.last_match[5]
609
+ return MU::Cloud::Azure.compute(credentials: credentials).virtual_machine_images.get(region, publisher, offer, sku, version)
610
+ else
611
+ publisher, offer, sku, version = image_id.split(/\//)
612
+ end
613
+ if !publisher or !offer or !sku
614
+ raise MuError, "Azure image_id #{image_id} was invalid"
615
+ end
616
+
617
+ skus = MU::Cloud::Azure.compute(credentials: credentials).virtual_machine_images.list_skus(region, publisher, offer).map { |s| s.name }
618
+
619
+ if !skus.include?(sku)
620
+ skus.sort { |a, b| MU.version_sort(a, b) }.reverse.each { |s|
621
+ if s.match(/^#{Regexp.quote(sku)}/)
622
+ sku = s
623
+ break
624
+ end
625
+ }
626
+ end
627
+
628
+ versions = MU::Cloud::Azure.compute(credentials: credentials).virtual_machine_images.list(region, publisher, offer, sku).map { |v| v.name }
629
+ if versions.nil? or versions.empty?
630
+ MU.log "Azure API returned empty machine image version list for publisher #{publisher} offer #{offer} sku #{sku}", MU::ERR
631
+ return nil
632
+ end
633
+
634
+ if version.nil?
635
+ version = versions.sort { |a, b| MU.version_sort(a, b) }.reverse.first
636
+ elsif !versions.include?(version)
637
+ versions.sort { |a, b| MU.version_sort(a, b) }.reverse.each { |v|
638
+ if v.match(/^#{Regexp.quote(version)}/)
639
+ version = v
640
+ break
641
+ end
642
+ }
643
+ end
644
+
645
+ MU::Cloud::Azure.compute(credentials: credentials).virtual_machine_images.get(region, publisher, offer, sku, version)
646
+ end
647
+
648
+ private
649
+
650
+ def create_update
651
+ ipcfg = MU::Cloud::Azure.network(:NetworkInterfaceIPConfiguration).new
652
+ ipcfg.name = @mu_name
653
+ ipcfg.private_ipallocation_method = MU::Cloud::Azure.network(:IPAllocationMethod)::Dynamic
654
+
655
+ private_nets = @vpc.subnets.reject { |s| !s.private? }
656
+ public_nets = @vpc.subnets.reject { |s| s.private? }
657
+
658
+ stubnet = if @config['vpc']['subnet_id']
659
+ useme = nil
660
+ @vpc.subnets.each { |s|
661
+ if s.cloud_id.to_s == @config['vpc']['subnet_id']
662
+ useme = s
663
+ break
664
+ end
665
+ }
666
+ if !useme
667
+ raise MuError, "Failed to locate subnet #{@config['vpc']['subnet_id']} in VPC #{@vpc.to_s}"
668
+ end
669
+ useme
670
+ elsif @config['vpc']['subnet_pref'] == "private" or
671
+ @config['vpc']['subnet_pref'] == "all_private"
672
+ if private_nets.size == 0
673
+ raise MuError, "Server #{@mu_name} wanted a private subnet, but there are none in #{@vpc.to_s}"
674
+ end
675
+ private_nets.sample
676
+ elsif @config['vpc']['subnet_pref'] == "public" or
677
+ @config['vpc']['subnet_pref'] == "all_public"
678
+ if public_nets.size == 0
679
+ raise MuError, "Server #{@mu_name} wanted a public subnet, but there are none in #{@vpc.to_s}"
680
+ end
681
+ public_nets.sample
682
+ end
683
+
684
+ # Allocate a public IP if we asked for one
685
+ if @config['associate_public_ip'] or !stubnet.private?
686
+ pubip_obj = MU::Cloud::Azure.network(:PublicIPAddress).new
687
+ pubip_obj.public_ipallocation_method = MU::Cloud::Azure.network(:IPAllocationMethod)::Dynamic
688
+ pubip_obj.location = @config['region']
689
+ pubip_obj.tags = @tags
690
+ resp = MU::Cloud::Azure.network(credentials: @credentials).public_ipaddresses.create_or_update(@resource_group, @mu_name, pubip_obj)
691
+ ipcfg.public_ipaddress = resp
692
+ end
693
+
694
+ ipcfg.subnet = MU::Cloud::Azure.network(:Subnet).new
695
+ ipcfg.subnet.id = stubnet.cloud_desc.id
696
+
697
+ sg = @deploy.findLitterMate(type: "firewall_rule", name: "server"+@config['name'])
698
+
699
+ iface_obj = MU::Cloud::Azure.network(:NetworkInterface).new
700
+ iface_obj.location = @config['region']
701
+ iface_obj.tags = @tags
702
+ iface_obj.primary = true
703
+ iface_obj.network_security_group = sg.cloud_desc if sg
704
+ iface_obj.enable_ipforwarding = !@config['src_dst_check']
705
+ iface_obj.ip_configurations = [ipcfg]
706
+ MU.log "Creating network interface #{@mu_name}", MU::DEBUG, details: iface_obj
707
+ iface = MU::Cloud::Azure.network(credentials: @credentials).network_interfaces.create_or_update(@resource_group, @mu_name, iface_obj)
708
+
709
+ img_obj = MU::Cloud::Azure.compute(:ImageReference).new
710
+ @config['image_id'].match(/\/Subscriptions\/[^\/]+\/Providers\/Microsoft.Compute\/Locations\/[^\/]+\/Publishers\/([^\/]+)\/ArtifactTypes\/VMImage\/Offers\/([^\/]+)\/Skus\/([^\/]+)\/Versions\/([^\/]+)$/)
711
+ img_obj.publisher = Regexp.last_match[1]
712
+ img_obj.offer = Regexp.last_match[2]
713
+ img_obj.sku = Regexp.last_match[3]
714
+ img_obj.version = Regexp.last_match[4]
715
+
716
+ hw_obj = MU::Cloud::Azure.compute(:HardwareProfile).new
717
+ hw_obj.vm_size = @config['size']
718
+
719
+ os_obj = MU::Cloud::Azure.compute(:OSProfile).new
720
+ os_obj.admin_username = @config['ssh_user']
721
+ os_obj.computer_name = @mu_name
722
+ if windows?
723
+ win_obj = MU::Cloud::Azure.compute(:WindowsConfiguration).new
724
+ os_obj.windows_configuration = win_obj
725
+ else
726
+ key_obj = MU::Cloud::Azure.compute(:SshPublicKey).new
727
+ key_obj.key_data = @deploy.ssh_public_key
728
+ key_obj.path = "/home/#{@config['ssh_user']}/.ssh/authorized_keys"
729
+
730
+ ssh_obj = MU::Cloud::Azure.compute(:SshConfiguration).new
731
+ ssh_obj.public_keys = [key_obj]
732
+
733
+ lnx_obj = MU::Cloud::Azure.compute(:LinuxConfiguration).new
734
+ lnx_obj.disable_password_authentication = true
735
+ lnx_obj.ssh = ssh_obj
736
+
737
+ os_obj.linux_configuration = lnx_obj
738
+ end
739
+
740
+ vm_id_obj = MU::Cloud::Azure.compute(:VirtualMachineIdentity).new
741
+ vm_id_obj.type = "UserAssigned"
742
+ svc_acct = @deploy.findLitterMate(type: "user", name: @config['name']+"user")
743
+ raise MuError, "Failed to locate service account #{@config['name']}user" if !svc_acct
744
+ vm_id_obj.user_assigned_identities = {
745
+ svc_acct.cloud_desc.id => svc_acct.cloud_desc
746
+ }
747
+
748
+ vm_obj = MU::Cloud::Azure.compute(:VirtualMachine).new
749
+ vm_obj.location = @config['region']
750
+ vm_obj.tags = @tags
751
+ vm_obj.network_profile = MU::Cloud::Azure.compute(:NetworkProfile).new
752
+ vm_obj.network_profile.network_interfaces = [iface]
753
+ vm_obj.hardware_profile = hw_obj
754
+ vm_obj.os_profile = os_obj
755
+ vm_obj.identity = vm_id_obj
756
+ vm_obj.storage_profile = MU::Cloud::Azure.compute(:StorageProfile).new
757
+ vm_obj.storage_profile.image_reference = img_obj
758
+
759
+ image_desc = MU::Cloud::Azure::Server.fetchImage(@config['image_id'].to_s, credentials: @config['credentials'], region: @config['region'])
760
+ # XXX do this as a catch around instance creation so we don't waste API calls
761
+ if image_desc.plan
762
+ terms = MU::Cloud::Azure.marketplace(credentials: @credentials).marketplace_agreements.get(image_desc.plan.publisher, image_desc.plan.product, image_desc.plan.name)
763
+ if !terms.accepted
764
+ MU.log "Agreeing to licensing terms of #{terms.product}", MU::NOTICE
765
+ begin
766
+ # XXX this doesn't actually work as documented
767
+ MU::Cloud::Azure.marketplace(credentials: @credentials).marketplace_agreements.sign(image_desc.plan.publisher, image_desc.plan.product, image_desc.plan.name)
768
+ rescue Exception => e
769
+ MU.log e.message, MU::ERR
770
+ vm_obj.plan = nil
771
+ end
772
+ end
773
+ vm_obj.plan = image_desc.plan
774
+ end
775
+ if @config['storage']
776
+ vm_obj.storage_profile.data_disks = []
777
+ @config['storage'].each { |disk|
778
+ lun = if disk['device'].is_a?(Integer) or
779
+ disk['device'].match(/^\d+$/)
780
+ disk['device'].to_i
781
+ else
782
+ disk['device'].match(/([a-z])[^a-z]*$/i)
783
+ # map the last letter of the requested device to a numeric lun
784
+ # so that a => 1, b => 2, and so on
785
+ Regexp.last_match[1].downcase.encode("ASCII-8BIT").ord - 96
786
+ end
787
+ disk_obj = MU::Cloud::Azure.compute(:DataDisk).new
788
+ disk_obj.disk_size_gb = disk['size']
789
+ disk_obj.lun = lun
790
+ disk_obj.name = @mu_name+disk['device'].to_s.gsub(/[^\w\-._]/, '_').upcase
791
+ disk_obj.create_option = MU::Cloud::Azure.compute(:DiskCreateOptionTypes)::Empty
792
+ vm_obj.storage_profile.data_disks << disk_obj
793
+ }
794
+ end
795
+
796
+
797
+ if !@cloud_id
798
+ # XXX actually guard this correctly
799
+ MU.log "Creating VM #{@mu_name}", details: vm_obj
800
+ vm = MU::Cloud::Azure.compute(credentials: @credentials).virtual_machines.create_or_update(@resource_group, @mu_name, vm_obj)
801
+ @cloud_id = Id.new(vm.id)
802
+ end
803
+
804
+ end
805
+
806
+
807
+ end #class
808
+ end #class
809
+ end
810
+ end #module