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
@@ -12,27 +12,136 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "net/http"
16
- require 'net/https'
17
- require 'multi_json'
18
- require 'stringio'
15
+ require 'open-uri'
16
+ require 'json'
17
+ require 'timeout'
19
18
 
20
19
  module MU
21
20
  class Cloud
22
21
  # Support for Microsoft Azure as a provisioning layer.
23
22
  class Azure
24
23
  @@is_in_azure = nil
24
+ @@metadata = nil
25
+ @@acct_to_profile_map = nil #WHAT EVEN IS THIS?
26
+ @@myRegion_var = nil
27
+ @@default_subscription = nil
28
+ @@regions = []
25
29
 
26
- # Alias for #{MU::Cloud::AWS.hosted?}
27
- def self.hosted
28
- MU::Cloud::Azure.hosted?
30
+ # Module used by {MU::Cloud} to insert additional instance methods into
31
+ # instantiated resources in this cloud layer.
32
+ module AdditionalResourceMethods
33
+ end
34
+
35
+ # Exception class for exclusive use by {MU::Cloud::Azure::SDKClient::ClientCallWrapper}
36
+ class APIError < MU::MuError
37
+ end
38
+
39
+ # Return a random Azure-valid GUID, because for some baffling reason some
40
+ # API calls expect us to roll our own.
41
+ def self.genGUID
42
+ hexchars = Array("a".."f") + Array(0..9)
43
+ guid_chunks = []
44
+ [8, 4, 4, 4, 12].each { |count|
45
+ guid_chunks << Array.new(count) { hexchars.sample }.join
46
+ }
47
+ guid_chunks.join("-")
29
48
  end
30
49
 
50
+ # A hook that is always called just before any of the instance method of
51
+ # our resource implementations gets invoked, so that we can ensure that
52
+ # repetitive setup tasks (like resolving +:resource_group+ for Azure
53
+ # resources) have always been done.
54
+ # @param cloudobj [MU::Cloud]
55
+ # @param deploy [MU::MommaCat]
56
+ def self.resourceInitHook(cloudobj, deploy)
57
+ class << self
58
+ attr_reader :resource_group
59
+ end
60
+ return if !cloudobj
61
+
62
+ rg = if !deploy
63
+ return if !hosted?
64
+ MU.myInstanceId.resource_group
65
+ else
66
+ region = cloudobj.config['region'] || MU::Cloud::Azure.myRegion(cloudobj.config['credentials'])
67
+ deploy.deploy_id+"-"+region.upcase
68
+ end
69
+
70
+ cloudobj.instance_variable_set(:@resource_group, rg)
71
+
72
+ end
73
+
74
+ # Any cloud-specific instance methods we require our resource implementations to have, above and beyond the ones specified by {MU::Cloud}
75
+ # @return [Array<Symbol>]
76
+ def self.required_instance_methods
77
+ [:resource_group]
78
+ end
79
+
80
+ # Stub class to represent Azure's resource identifiers, which look like:
81
+ # /subscriptions/3d20ddd8-4652-4074-adda-0d127ef1f0e0/resourceGroups/mu/providers/Microsoft.Network/virtualNetworks/mu-vnet
82
+ # Various API calls need chunks of this in different contexts, and this
83
+ # full string is necessary to guarantee that a +cloud_id+ is a unique
84
+ # identifier for a given resource. So we'll use this object of our own
85
+ # devising to represent it.
86
+ class Id
87
+ attr_reader :subscription
88
+ attr_reader :resource_group
89
+ attr_reader :provider
90
+ attr_reader :type
91
+ attr_reader :name
92
+ attr_reader :raw
93
+
94
+ # The name of the attribute on a cloud object from this provider which
95
+ # has the provider's long-form cloud identifier (Google Cloud URL,
96
+ # Amazon ARN, etc).
97
+ def self.idattr
98
+ :id
99
+ end
100
+
101
+ def initialize(*args)
102
+ if args.first.is_a?(String)
103
+ @raw = args.first
104
+ junk, junk, @subscription, junk, @resource_group, junk, @provider, @resource_type, @name = @raw.split(/\//)
105
+ if @subscription.nil? or @resource_group.nil? or @provider.nil? or @resource_type.nil? or @name.nil?
106
+ # Not everything has a resource group
107
+ if @raw.match(/^\/subscriptions\/#{Regexp.quote(@subscription)}\/providers/)
108
+ junk, junk, @subscription, junk, @provider, @resource_type, @name = @raw.split(/\//)
109
+ if @subscription.nil? or @provider.nil? or @resource_type.nil? or @name.nil?
110
+ raise MuError, "Failed to parse Azure resource id string #{@raw} (got subscription: #{@subscription}, provider: #{@provider}, resource_type: #{@resource_type}, name: #{@name}"
111
+ end
112
+
113
+ else
114
+ raise MuError, "Failed to parse Azure resource id string #{@raw} (got subscription: #{@subscription}, resource_group: #{@resource_group}, provider: #{@provider}, resource_type: #{@resource_type}, name: #{@name}"
115
+ end
116
+ end
117
+ else
118
+ args.each { |arg|
119
+ if arg.is_a?(Hash)
120
+ arg.each_pair { |k, v|
121
+ self.instance_variable_set(("@"+k.to_s).to_sym, v)
122
+ }
123
+ end
124
+ }
125
+
126
+ if @resource_group.nil? or @name.nil?
127
+ raise MuError, "Failed to extract at least name and resource_group fields from #{args.flatten.join(", ").to_s}"
128
+ end
129
+ end
130
+ end
131
+
132
+ # Return a reasonable string representation of this {MU::Cloud::Azure::Id}
133
+ def to_s
134
+ @name
135
+ end
136
+ end
137
+
138
+
139
+ # UTILITY METHODS
31
140
  # Determine whether we (the Mu master, presumably) are hosted in Azure.
32
141
  # @return [Boolean]
33
142
  def self.hosted?
34
- if $MU_CFG.has_key?("azure_is_hosted")
35
- @@is_in_aws = $MU_CFG["azure_is_hosted"]
143
+ if $MU_CFG and $MU_CFG.has_key?("azure_is_hosted")
144
+ @@is_in_azure = $MU_CFG["azure_is_hosted"]
36
145
  return $MU_CFG["azure_is_hosted"]
37
146
  end
38
147
 
@@ -56,52 +165,330 @@ module MU
56
165
  false
57
166
  end
58
167
 
168
+ # If we reside in this cloud, return the VPC in which we, the Mu Master, reside.
169
+ # @return [MU::Cloud::VPC]
170
+ def self.myVPC
171
+ return nil if !hosted?
172
+ # XXX do me
173
+ end
174
+
175
+ # Alias for #{MU::Cloud::Azure.hosted?}
176
+ def self.hosted
177
+ return MU::Cloud::Azure.hosted?
178
+ end
179
+
180
+ # If we're running this cloud, return the $MU_CFG blob we'd use to
181
+ # describe this environment as our target one.
59
182
  def self.hosted_config
60
- "TODO"
183
+ return nil if !hosted?
184
+ region = get_metadata()['compute']['location']
185
+ subscription = get_metadata()['compute']['subscriptionId']
186
+ {
187
+ "region" => region,
188
+ "subscriptionId" => subscription
189
+ }
61
190
  end
62
191
 
63
- # Any cloud-specific instance methods we require our resource implementations to have, above and beyond the ones specified by {MU::Cloud}
64
- # @return [Array<Symbol>]
65
- def self.required_instance_methods
66
- []
192
+ # Azure's API response objects don't implement +to_h+, so we'll wing it
193
+ # ourselves
194
+ # @param struct [MsRestAzure]
195
+ # @return [Hash]
196
+ def self.respToHash(struct)
197
+ hash = {}
198
+ struct.class.instance_methods(false).each { |m|
199
+ next if m.to_s.match(/=$/)
200
+ hash[m.to_s] = struct.send(m)
201
+ }
202
+ struct.instance_variables.each { |a|
203
+ hash[a.to_s.sub(/^@/, "")] = struct.instance_variable_get(a)
204
+ }
205
+ hash
67
206
  end
68
207
 
69
- def self.myRegion
70
- "TODO"
208
+ # Method that returns the default Azure region for this Mu Master
209
+ # @return [string]
210
+ def self.myRegion(credentials = nil)
211
+ if @@myRegion_var
212
+ return @@myRegion_var
213
+ end
214
+
215
+ cfg = credConfig(credentials)
216
+
217
+ @@myRegion_var = if cfg['default_region']
218
+ cfg['default_region']
219
+ elsif MU::Cloud::Azure.hosted?
220
+ # IF WE ARE HOSTED IN AZURE CHECK FOR THE REGION OF THE INSTANCE
221
+ metadata = get_metadata()
222
+ metadata['compute']['location']
223
+ else
224
+ "eastus"
225
+ end
226
+
227
+ return @@myRegion_var
228
+ end
229
+
230
+ # lookup the default subscription that will be used by methods
231
+ def self.default_subscription(credentials = nil)
232
+ cfg = credConfig(credentials)
233
+ if @@default_subscription.nil?
234
+ if cfg['subscription']
235
+ # MU.log "Found default subscription in mu.yml. Using that..."
236
+ @@default_subscription = cfg['subscription']
237
+
238
+ elsif listSubscriptions().length == 1
239
+ #MU.log "Found a single subscription on your account. Using that... (This may be incorrect)", MU::WARN, details: e.message
240
+ @@default_subscription = listSubscriptions()[0]
241
+
242
+ elsif MU::Cloud::Azure.hosted?
243
+ #MU.log "Found a subscriptionID in my metadata. Using that... (This may be incorrect)", MU::WARN, details: e.message
244
+ @@default_subscription = get_metadata()['compute']['subscriptionId']
245
+
246
+ else
247
+ raise MuError, "Default Subscription was not found. Please run mu-configure to setup a default subscription"
248
+ end
249
+ end
250
+
251
+ return @@default_subscription
252
+ end
253
+
254
+ # List visible Azure regions
255
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
256
+ # return [Array<String>]
257
+ def self.listRegions(credentials: nil)
258
+ cfg = credConfig(credentials)
259
+ return nil if !cfg and !hosted?
260
+ subscription = cfg['subscription']
261
+ subscription ||= default_subscription()
262
+
263
+ if @@regions.length() > 0 && subscription == default_subscription()
264
+ return @@regions
265
+ end
266
+
267
+ begin
268
+ sdk_response = MU::Cloud::Azure.subs(credentials: credentials).subscriptions().list_locations(subscription)
269
+ rescue Exception => e
270
+ MU.log e.inspect, MU::ERR, details: e.backtrace
271
+ #pp "Error Getting the list of regions from Azure" #TODO: SWITCH THIS TO MU LOG
272
+ return @@regions if @@regions and @@regions.size > 0
273
+ raise e
274
+ end
275
+
276
+ sdk_response.value.each do | region |
277
+ @@regions.push(region.name)
278
+ end
279
+
280
+ return @@regions
71
281
  end
72
282
 
73
- def self.listRegions(credentials = nil)
74
- []
283
+ # List subscriptions visible to the given credentials
284
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
285
+ # return [Array<String>]
286
+ def self.listSubscriptions(credentials = nil)
287
+ subscriptions = []
288
+
289
+ sdk_response = MU::Cloud::Azure.subs(credentials: credentials).subscriptions().list
290
+
291
+ sdk_response.each do |subscription|
292
+ subscriptions.push(subscription.subscription_id)
293
+ end
294
+
295
+ return subscriptions
75
296
  end
76
297
 
298
+ # List the Availability Zones associated with a given Azure region.
299
+ # If no region is given, search the one in which this MU master
300
+ # server resides (if it resides in this cloud provider's ecosystem).
301
+ # @param region [String]: The region to search.
302
+ # @return [Array<String>]: The Availability Zones in this region.
77
303
  def self.listAZs(region = nil)
78
- []
304
+ az_list = ['1', '2', '3']
305
+
306
+ # Pulled from this chart: https://docs.microsoft.com/en-us/azure/availability-zones/az-overview#services-support-by-region
307
+ az_enabled_regions = ['centralus', 'eastus', 'eastus2', 'westus2', 'francecentral', 'northeurope', 'uksouth', 'westeurope', 'japaneast', 'southeastasia']
308
+
309
+ if not az_enabled_regions.include?(region)
310
+ az_list = []
311
+ end
312
+
313
+ return az_list
79
314
  end
80
315
 
316
+ # A non-working example configuration
81
317
  def self.config_example
82
- {}
318
+ sample = hosted_config
319
+ sample ||= {
320
+ "region" => "eastus",
321
+ "subscriptionId" => "99999999-9999-9999-9999-999999999999",
322
+ }
323
+
324
+ sample["credentials_file"] = "~/.azure/credentials"
325
+ sample["log_bucket_name"] = "my-mu-s3-bucket"
326
+ sample
83
327
  end
84
328
 
85
- def self.writeDeploySecret
86
- "TODO"
329
+ # Do cloud-specific deploy instantiation tasks, such as copying SSH keys
330
+ # around, sticking secrets in buckets, creating resource groups, etc
331
+ # @param deploy [MU::MommaCat]
332
+ def self.initDeploy(deploy)
333
+ deploy.credsUsed.each { |creds|
334
+ next if !credConfig(creds)
335
+ listRegions.each { |region|
336
+ next if !deploy.regionsUsed.include?(region)
337
+ begin
338
+ createResourceGroup(deploy.deploy_id+"-"+region.upcase, region, credentials: creds)
339
+ rescue ::MsRestAzure::AzureOperationError
340
+ end
341
+ }
342
+ }
87
343
  end
88
344
 
345
+ @@rg_semaphore = Mutex.new
346
+
347
+ # Purge cloud-specific deploy meta-artifacts (SSH keys, resource groups,
348
+ # etc)
349
+ # @param deploy_id [String]
350
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
351
+ def self.cleanDeploy(deploy_id, credentials: nil, noop: false)
352
+ threads = []
353
+
354
+ @@rg_semaphore.synchronize {
355
+ MU::Cloud::Azure.resources(credentials: credentials).resource_groups.list.each { |rg|
356
+ if rg.tags and rg.tags["MU-ID"] == deploy_id
357
+ threads << Thread.new(rg) { |rg_obj|
358
+ Thread.abort_on_exception = false
359
+ MU.log "Removing resource group #{rg_obj.name} from #{rg_obj.location}"
360
+ if !noop
361
+ MU::Cloud::Azure.resources(credentials: credentials).resource_groups.delete(rg_obj.name)
362
+ end
363
+ }
364
+ end
365
+ }
366
+ threads.each { |t|
367
+ t.join
368
+ }
369
+ }
370
+ end
371
+
372
+ # Azure resources are deployed into a containing artifact called a Resource Group, which we will map 1:1 with Mu deployments
373
+ # @param name [String]: A name for this resource group
374
+ # @param region [String]: The region in which to create this resource group
375
+ def self.createResourceGroup(name, region, credentials: nil)
376
+ rg_obj = MU::Cloud::Azure.resources(:ResourceGroup).new
377
+ rg_obj.location = region
378
+ rg_obj.tags = MU::MommaCat.listStandardTags
379
+
380
+ MU::Cloud::Azure.resources(credentials: credentials).resource_groups.list.each { |rg|
381
+ if rg.name == name and rg.location == region and rg.tags == rg_obj.tags
382
+ MU.log "Resource group #{name} already exists in #{region}", MU::DEBUG, details: rg_obj
383
+ return rg # already exists? Do nothing
384
+ end
385
+ }
386
+ MU.log "Configuring resource group #{name} in #{region}", details: rg_obj
387
+
388
+ MU::Cloud::Azure.resources(credentials: credentials).resource_groups.create_or_update(
389
+ name,
390
+ rg_obj
391
+ )
392
+ end
393
+
394
+ # Plant a Mu deploy secret into a storage bucket somewhere for so our kittens can consume it
395
+ # @param deploy_id [String]: The deploy for which we're writing the secret
396
+ # @param value [String]: The contents of the secret
397
+ def self.writeDeploySecret(deploy_id, value, name = nil, credentials: nil)
398
+ # XXX this ain't it hoss
399
+ end
400
+
401
+ # Return the name strings of all known sets of credentials for this cloud
402
+ # @return [Array<String>]
89
403
  def self.listCredentials
90
- "TODO"
404
+ if !$MU_CFG['azure']
405
+ return hosted? ? ["#default"] : nil
406
+ end
407
+
408
+ $MU_CFG['azure'].keys
91
409
  end
92
410
 
93
- def self.credConfig
94
- "TODO"
411
+ # Return what we think of as a cloud object's habitat. If this is not
412
+ # applicable, such as for a {Habitat} or {Folder}, returns nil.
413
+ # @param cloudobj [MU::Cloud::Azure]: The resource from which to extract the habitat id
414
+ # @return [String,nil]
415
+ def self.habitat(cloudobj, nolookup: false, deploy: nil)
416
+ nil # we don't know how to do anything with subscriptions yet, really
95
417
  end
96
418
 
97
- def self.listInstanceTypes
98
- "TODO"
419
+ @@my_hosted_cfg = nil
420
+ # Return the $MU_CFG data associated with a particular profile/name/set of
421
+ # credentials. If no account name is specified, will return one flagged as
422
+ # default. Returns nil if Azure is not configured. Throws an exception if
423
+ # an account name is specified which does not exist.
424
+ # @param name [String]: The name of the key under 'azure' in mu.yaml to return
425
+ # @return [Hash,nil]
426
+ def self.credConfig (name = nil, name_only: false)
427
+ if !$MU_CFG['azure'] or !$MU_CFG['azure'].is_a?(Hash) or $MU_CFG['azure'].size == 0
428
+ return @@my_hosted_cfg if @@my_hosted_cfg
429
+
430
+ if hosted?
431
+ @@my_hosted_cfg = hosted_config
432
+ return name_only ? "#default" : @@my_hosted_cfg
433
+ end
434
+
435
+ return nil
436
+ end
437
+
438
+ if name.nil?
439
+ $MU_CFG['azure'].each_pair { |set, cfg|
440
+ if cfg['default']
441
+ return name_only ? set : cfg
442
+ end
443
+ }
444
+ else
445
+ if $MU_CFG['azure'][name]
446
+ return name_only ? name : $MU_CFG['azure'][name]
447
+ # elsif @@acct_to_profile_map[name.to_s]
448
+ # return name_only ? name : @@acct_to_profile_map[name.to_s]
449
+ end
450
+ # XXX whatever process might lead us to populate @@acct_to_profile_map with some mappings, like projectname -> account profile, goes here
451
+ return nil
452
+ end
453
+
99
454
  end
100
-
455
+
456
+ @@instance_types = nil
457
+ # Query the Azure API for a list of valid instance types.
458
+ # @param region [String]: Supported machine types can vary from region to region, so we look for the set we're interested in specifically
459
+ # @return [Hash]
460
+ def self.listInstanceTypes(region = self.myRegion)
461
+ return @@instance_types if @@instance_types and @@instance_types[region]
462
+ if !MU::Cloud::Azure.default_subscription()
463
+ return {}
464
+ end
465
+
466
+ @@instance_types ||= {}
467
+ @@instance_types[region] ||= {}
468
+ result = MU::Cloud::Azure.compute.virtual_machine_sizes.list(region)
469
+ raise MuError, "Failed to fetch Azure instance type list" if !result
470
+ result.value.each { |type|
471
+ @@instance_types[region][type.name] ||= {}
472
+ @@instance_types[region][type.name]["memory"] = sprintf("%.1f", type.memory_in_mb/1024.0).to_f
473
+ @@instance_types[region][type.name]["vcpu"] = type.number_of_cores.to_f
474
+ @@instance_types[region][type.name]["ecu"] = type.number_of_cores
475
+ }
476
+
477
+ @@instance_types
478
+ end
479
+
480
+ # Resolve the administrative Cloud Storage bucket for a given credential
481
+ # set, or return a default.
482
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
483
+ # @return [String]
101
484
  def self.adminBucketName(credentials = nil)
102
485
  "TODO"
103
486
  end
104
487
 
488
+ # Resolve the administrative Cloud Storage bucket for a given credential
489
+ # set, or return a default.
490
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
491
+ # @return [String]
105
492
  def self.adminBucketUrl(credentials = nil)
106
493
  "TODO"
107
494
  end
@@ -109,28 +496,575 @@ module MU
109
496
  #END REQUIRED METHODS
110
497
 
111
498
 
112
- # Fetch an Azure instance metadata parameter (example: public-ipv4).
113
- # @return [String, nil]
114
- def self.get_metadata()
115
- base_url = "http://169.254.169.254/metadata/instance"
116
- api_version = '2017-12-01'
499
+ # Fetch (ALL) Azure instance metadata
500
+ # @return [Hash, nil]
501
+ def self.get_metadata(svc = "instance", api_version = "2017-08-01", args: {}, debug: false)
502
+ loglevel = debug ? MU::NOTICE : MU::DEBUG
503
+ return @@metadata if svc == "instance" and @@metadata
504
+ base_url = "http://169.254.169.254/metadata/#{svc}"
505
+ args["api-version"] = api_version
506
+ arg_str = args.keys.map { |k| k.to_s+"="+args[k].to_s }.join("&")
507
+
117
508
  begin
118
- response = nil
119
- Timeout.timeout(1) do
120
- response = MultiJson.load(open("#{base_url}/?api-version=#{ api_version }", "Metadata" => "true").read)
509
+ Timeout.timeout(2) do
510
+ resp = JSON.parse(open("#{base_url}/?#{arg_str}","Metadata"=>"true").read)
511
+ MU.log "curl -H Metadata:true "+"#{base_url}/?#{arg_str}", loglevel, details: resp
512
+ if svc != "instance"
513
+ return resp
514
+ else
515
+ @@metadata = resp
516
+ end
121
517
  end
518
+ return @@metadata
122
519
 
123
- response
124
- rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH, Net::HTTPServerException, Errno::EHOSTUNREACH => e
125
- # This is normal on machines checking to see if they're AWS-hosted
126
- logger = MU::Logger.new
127
- logger.log "Failed metadata request #{base_url}/: #{e.inspect}", MU::DEBUG
520
+ rescue Timeout::Error => e
521
+ # MU.log "Timeout querying Azure Metadata"
522
+ return nil
523
+ rescue
524
+ # MU.log "Failed to get Azure MetaData."
128
525
  return nil
129
526
  end
130
527
  end
131
- end
132
- end
133
- end
134
528
 
529
+ # Map our SDK authorization options from MU configuration into an options
530
+ # hash that Azure understands. Raises an exception if any fields aren't
531
+ # available.
532
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
533
+ # @return [Hash]
534
+ def self.getSDKOptions(credentials = nil)
535
+ cfg = credConfig(credentials)
536
+
537
+ if cfg and MU::Cloud::Azure.hosted?
538
+ token = MU::Cloud::Azure.get_metadata("identity/oauth2/token", "2018-02-01", args: { "resource"=>"https://management.azure.com/" })
539
+ if !token
540
+ MU::Cloud::Azure.get_metadata("identity/oauth2/token", "2018-02-01", args: { "resource"=>"https://management.azure.com/" }, debug: true)
541
+ raise MuError, "Failed to get machine oauth token"
542
+ end
543
+ machine = MU::Cloud::Azure.get_metadata
544
+ return {
545
+ credentials: MsRest::TokenCredentials.new(token["access_token"]),
546
+ client_id: token["client_id"],
547
+ subscription: machine["compute"]["subscriptionId"],
548
+ subscription_id: machine["compute"]["subscriptionId"]
549
+ }
550
+ end
551
+
552
+ return nil if !cfg
553
+
554
+ map = { #... from mu.yaml-ese to Azure SDK-ese
555
+ "directory_id" => :tenant_id,
556
+ "client_id" => :client_id,
557
+ "client_secret" => :client_secret,
558
+ "subscription" => :subscription_id
559
+ }
135
560
 
561
+ options = {}
136
562
 
563
+ map.each_pair { |k, v|
564
+ options[v] = cfg[k] if cfg[k]
565
+ }
566
+
567
+ if cfg['credentials_file']
568
+ file = File.open cfg['credentials_file']
569
+ credfile = JSON.load file
570
+ map.each_pair { |k, v|
571
+ options[v] = credfile[k] if credfile[k]
572
+ }
573
+ end
574
+
575
+ missing = []
576
+ map.values.each { |v|
577
+ missing << v if !options[v]
578
+ }
579
+
580
+ if missing.size > 0
581
+ if (!credentials or credentials == "#default") and hosted?
582
+ # Let the SDK try to use machine credentials
583
+ return nil
584
+ end
585
+ raise MuError, "Missing fields while trying to load Azure SDK options for credential set #{credentials ? credentials : "<default>" }: #{missing.map { |m| m.to_s }.join(", ")}"
586
+ end
587
+
588
+ MU.log "Loaded credential set #{credentials ? credentials : "<default>" }", MU::DEBUG, details: options
589
+
590
+ return options
591
+ end
592
+
593
+ # Find or allocate a static public IP address resource
594
+ # @param resource_group [String]
595
+ # @param name [String]
596
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
597
+ # @param region [String]
598
+ # @param tags [Hash<String>]
599
+ # @return [Azure::Network::Mgmt::V2019_02_01::Models::PublicIPAddress]
600
+ def self.fetchPublicIP(resource_group, name, credentials: nil, region: nil, tags: nil)
601
+ if !name or !resource_group
602
+ raise MuError, "Must supply resource_group and name to create or retrieve an Azure PublicIPAddress"
603
+ end
604
+ region ||= MU::Cloud::Azure.myRegion(credentials)
605
+
606
+ resp = MU::Cloud::Azure.network(credentials: credentials).public_ipaddresses.get(resource_group, name)
607
+ if !resp
608
+ ip_obj = MU::Cloud::Azure.network(:PublicIPAddress).new
609
+ ip_obj.location = region
610
+ ip_obj.tags = tags if tags
611
+ ip_obj.public_ipallocation_method = "Dynamic"
612
+ MU.log "Allocating PublicIpAddress #{name}", details: ip_obj
613
+ resp = MU::Cloud::Azure.network(credentials: credentials).public_ipaddresses.create_or_update(resource_group, name, ip_obj)
614
+ end
615
+
616
+ resp
617
+ end
618
+
619
+ # BEGIN SDK STUBS
620
+ #
621
+ # Azure Subscription Manager API
622
+ # @param model [<Azure::Apis::Subscriptions::Mgmt::V2015_11_01::Models>]: If specified, will return the class ::Azure::Apis::Subscriptions::Mgmt::V2015_11_01::Models::model instead of an API client instance
623
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
624
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
625
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
626
+ # @return [MU::Cloud::Azure::SDKClient]
627
+ def self.subs(model = nil, alt_object: nil, credentials: nil, model_version: "V2015_11_01")
628
+ require 'azure_mgmt_subscriptions'
629
+
630
+ if model and model.is_a?(Symbol)
631
+ return Object.const_get("Azure").const_get("Subscriptions").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
632
+ else
633
+ @@subscriptions_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Subscriptions", credentials: credentials, subclass: alt_object)
634
+ end
635
+
636
+ return @@subscriptions_api[credentials]
637
+ end
638
+
639
+ # An alternative version of the Azure Subscription Manager API, which appears to support subscription creation
640
+ # @param model [<Azure::Apis::Subscriptions::Mgmt::V2018_03_01_preview::Models>]: If specified, will return the class ::Azure::Apis::Subscriptions::Mgmt::V2018_03_01_preview::Models::model instead of an API client instance
641
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
642
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
643
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
644
+ # @return [MU::Cloud::Azure::SDKClient]
645
+ def self.subfactory(model = nil, alt_object: nil, credentials: nil, model_version: "V2018_03_01_preview")
646
+ require 'azure_mgmt_subscriptions'
647
+
648
+ if model and model.is_a?(Symbol)
649
+ return Object.const_get("Azure").const_get("Subscriptions").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
650
+ else
651
+ @@subscriptions_factory_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Subscriptions", credentials: credentials, profile: "V2018_03_01_preview", subclass: alt_object)
652
+ end
653
+
654
+ return @@subscriptions_factory_api[credentials]
655
+ end
656
+
657
+ # The Azure Compute API
658
+ # @param model [<Azure::Apis::Compute::Mgmt::V2019_04_01::Models>]: If specified, will return the class ::Azure::Apis::Compute::Mgmt::V2019_04_01::Models::model instead of an API client instance
659
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
660
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
661
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
662
+ # @return [MU::Cloud::Azure::SDKClient]
663
+ def self.compute(model = nil, alt_object: nil, credentials: nil, model_version: "V2019_03_01")
664
+ require 'azure_mgmt_compute'
665
+
666
+ if model and model.is_a?(Symbol)
667
+ return Object.const_get("Azure").const_get("Compute").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
668
+ else
669
+ @@compute_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Compute", credentials: credentials, subclass: alt_object)
670
+ end
671
+
672
+ return @@compute_api[credentials]
673
+ end
674
+
675
+ # The Azure Network API
676
+ # @param model [<Azure::Apis::Network::Mgmt::V2019_02_01::Models>]: If specified, will return the class ::Azure::Apis::Network::Mgmt::V2019_02_01::Models::model instead of an API client instance
677
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
678
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
679
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
680
+ # @return [MU::Cloud::Azure::SDKClient]
681
+ def self.network(model = nil, alt_object: nil, credentials: nil, model_version: "V2019_02_01")
682
+ require 'azure_mgmt_network'
683
+
684
+ if model and model.is_a?(Symbol)
685
+ return Object.const_get("Azure").const_get("Network").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
686
+ else
687
+ @@network_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Network", credentials: credentials, subclass: alt_object)
688
+ end
689
+
690
+ return @@network_api[credentials]
691
+ end
692
+
693
+ # The Azure Storage API
694
+ # @param model [<Azure::Apis::Storage::Mgmt::V2019_04_01::Models>]: If specified, will return the class ::Azure::Apis::Storage::Mgmt::V2019_04_01::Models::model instead of an API client instance
695
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
696
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
697
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
698
+ # @return [MU::Cloud::Azure::SDKClient]
699
+ def self.storage(model = nil, alt_object: nil, credentials: nil, model_version: "V2019_04_01")
700
+ require 'azure_mgmt_storage'
701
+
702
+ if model and model.is_a?(Symbol)
703
+ return Object.const_get("Azure").const_get("Storage").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
704
+ else
705
+ @@storage_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Storage", credentials: credentials, subclass: alt_object)
706
+ end
707
+
708
+ return @@storage_api[credentials]
709
+ end
710
+
711
+ # The Azure ApiManagement API
712
+ # @param model [<Azure::Apis::ApiManagement::Mgmt::V2019_01_01::Models>]: If specified, will return the class ::Azure::Apis::ApiManagement::Mgmt::V2019_01_01::Models::model instead of an API client instance
713
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
714
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
715
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
716
+ # @return [MU::Cloud::Azure::SDKClient]
717
+ def self.apis(model = nil, alt_object: nil, credentials: nil, model_version: "V2019_01_01")
718
+ require 'azure_mgmt_api_management'
719
+
720
+ if model and model.is_a?(Symbol)
721
+ return Object.const_get("Azure").const_get("ApiManagement").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
722
+ else
723
+ @@apis_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "ApiManagement", credentials: credentials, subclass: alt_object)
724
+ end
725
+
726
+ return @@apis_api[credentials]
727
+ end
728
+
729
+ # The Azure MarketplaceOrdering API
730
+ # @param model [<Azure::Apis::MarketplaceOrdering::Mgmt::V2015_06_01::Models>]: If specified, will return the class ::Azure::Apis::MarketplaceOrdering::Mgmt::V2015_06_01::Models::model instead of an API client instance
731
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
732
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
733
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
734
+ # @return [MU::Cloud::Azure::SDKClient]
735
+ def self.marketplace(model = nil, alt_object: nil, credentials: nil, model_version: "V2015_06_01")
736
+ require 'azure_mgmt_marketplace_ordering'
737
+
738
+ if model and model.is_a?(Symbol)
739
+ return Object.const_get("Azure").const_get("Resources").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
740
+ else
741
+ @@marketplace_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "MarketplaceOrdering", credentials: credentials, subclass: alt_object)
742
+ end
743
+
744
+ return @@marketplace_api[credentials]
745
+ end
746
+
747
+ # The Azure Resources API
748
+ # @param model [<Azure::Apis::Resources::Mgmt::V2018_05_01::Models>]: If specified, will return the class ::Azure::Apis::Resources::Mgmt::V2018_05_01::Models::model instead of an API client instance
749
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
750
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
751
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
752
+ # @return [MU::Cloud::Azure::SDKClient]
753
+ def self.resources(model = nil, alt_object: nil, credentials: nil, model_version: "V2018_05_01")
754
+ require 'azure_mgmt_resources'
755
+
756
+ if model and model.is_a?(Symbol)
757
+ return Object.const_get("Azure").const_get("Resources").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
758
+ else
759
+ @@resources_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Resources", credentials: credentials, subclass: alt_object)
760
+ end
761
+
762
+ return @@resources_api[credentials]
763
+ end
764
+
765
+ # The Azure Features API
766
+ # @param model [<Azure::Apis::Features::Mgmt::V2015_12_01::Models>]: If specified, will return the class ::Azure::Apis::Features::Mgmt::V2015_12_01::Models::model instead of an API client instance
767
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
768
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
769
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
770
+ # @return [MU::Cloud::Azure::SDKClient]
771
+ def self.features(model = nil, alt_object: nil, credentials: nil, model_version: "V2015_12_01")
772
+ require 'azure_mgmt_features'
773
+
774
+ if model and model.is_a?(Symbol)
775
+ return Object.const_get("Azure").const_get("Features").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
776
+ else
777
+ @@features_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Features", credentials: credentials, subclass: alt_object)
778
+ end
779
+
780
+ return @@features_api[credentials]
781
+ end
782
+
783
+ # The Azure ContainerService API
784
+ # @param model [<Azure::Apis::ContainerService::Mgmt::V2019_04_01::Models>]: If specified, will return the class ::Azure::Apis::ContainerService::Mgmt::V2019_04_01::Models::model instead of an API client instance
785
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
786
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
787
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
788
+ # @return [MU::Cloud::Azure::SDKClient]
789
+ def self.containers(model = nil, alt_object: nil, credentials: nil, model_version: "V2019_04_01")
790
+ require 'azure_mgmt_container_service'
791
+
792
+ if model and model.is_a?(Symbol)
793
+ return Object.const_get("Azure").const_get("ContainerService").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
794
+ else
795
+ @@containers_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "ContainerService", credentials: credentials, subclass: alt_object)
796
+ end
797
+
798
+ return @@containers_api[credentials]
799
+ end
800
+
801
+ # The Azure ManagedServiceIdentity API
802
+ # @param model [<Azure::Apis::ManagedServiceIdentity::Mgmt::V2015_08_31_preview::Models>]: If specified, will return the class ::Azure::Apis::ManagedServiceIdentity::Mgmt::V2015_08_31_preview::Models::model instead of an API client instance
803
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
804
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
805
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
806
+ # @return [MU::Cloud::Azure::SDKClient]
807
+ def self.serviceaccts(model = nil, alt_object: nil, credentials: nil, model_version: "V2015_08_31_preview")
808
+ require 'azure_mgmt_msi'
809
+
810
+ if model and model.is_a?(Symbol)
811
+ return Object.const_get("Azure").const_get("ManagedServiceIdentity").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
812
+ else
813
+ @@service_identity_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "ManagedServiceIdentity", credentials: credentials, subclass: alt_object)
814
+ end
815
+
816
+ return @@service_identity_api[credentials]
817
+ end
818
+
819
+ # The Azure Authorization API
820
+ # @param model [<Azure::Apis::Authorization::Mgmt::V2015_07_01::Models>]: If specified, will return the class ::Azure::Apis::Authorization::Mgmt::V2015_07_01::Models::model instead of an API client instance
821
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
822
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
823
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
824
+ # @return [MU::Cloud::Azure::SDKClient]
825
+ def self.authorization(model = nil, alt_object: nil, credentials: nil, model_version: "V2015_07_01", endpoint_profile: "Latest")
826
+ require 'azure_mgmt_authorization'
827
+
828
+ if model and model.is_a?(Symbol)
829
+ return Object.const_get("Azure").const_get("Authorization").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
830
+ else
831
+ @@authorization_api[credentials] ||= {}
832
+ @@authorization_api[credentials][endpoint_profile] ||= MU::Cloud::Azure::SDKClient.new(api: "Authorization", credentials: credentials, subclass: "AuthorizationManagementClass", profile: endpoint_profile)
833
+ end
834
+
835
+ return @@authorization_api[credentials][endpoint_profile]
836
+ end
837
+
838
+ # The Azure Billing API
839
+ # @param model [<Azure::Apis::Billing::Mgmt::V2018_03_01_preview::Models>]: If specified, will return the class ::Azure::Apis::Billing::Mgmt::V2018_03_01_preview::Models::model instead of an API client instance
840
+ # @param model_version [String]: Use an alternative model version supported by the SDK when requesting a +model+
841
+ # @param alt_object [String]: Return an instance of something other than the usual API client object
842
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
843
+ # @return [MU::Cloud::Azure::SDKClient]
844
+ def self.billing(model = nil, alt_object: nil, credentials: nil, model_version: "V2018_03_01_preview")
845
+ require 'azure_mgmt_billing'
846
+
847
+ if model and model.is_a?(Symbol)
848
+ return Object.const_get("Azure").const_get("Billing").const_get("Mgmt").const_get(model_version).const_get("Models").const_get(model)
849
+ else
850
+ @@billing_api[credentials] ||= MU::Cloud::Azure::SDKClient.new(api: "Billing", credentials: credentials, subclass: alt_object)
851
+ end
852
+
853
+ return @@billing_api[credentials]
854
+ end
855
+
856
+ # Make sure that a provider is enabled ("Registered" in Azure-ese).
857
+ # @param provider [String]: Provider name, typically formatted like +Microsoft.ContainerService+
858
+ # @param force [Boolean]: Run the operation even if the provider already appears to be enabled
859
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
860
+ def self.ensureProvider(provider, force: false, credentials: nil)
861
+ state = MU::Cloud::Azure.resources(credentials: credentials).providers.get(provider)
862
+ if state.registration_state != "Registered" or force
863
+ begin
864
+ if state.registration_state == "NotRegistered" or force
865
+ MU.log "Registering Provider #{provider}", MU::NOTICE
866
+ MU::Cloud::Azure.resources(credentials: credentials).providers.register(provider)
867
+ force = false
868
+ sleep 30
869
+ elsif state.registration_state == "Registering"
870
+ MU.log "Waiting for Provider #{provider} to finish registering", MU::NOTICE, details: state.registration_state
871
+ sleep 30
872
+ end
873
+ state = MU::Cloud::Azure.resources(credentials: credentials).providers.get(provider)
874
+ end while state and state.registration_state != "Registered"
875
+ end
876
+ end
877
+
878
+ # Make sure that a feature is enabled ("Registered" in Azure-ese), usually invoked for preview features which are off by default.
879
+ # @param feature_string [String]: The name of a feature, such as +WindowsPreview+
880
+ # @param credentials [String]: The credential set (subscription, effectively) in which to operate
881
+ def self.ensureFeature(feature_string, credentials: nil)
882
+ provider, feature = feature_string.split(/\//)
883
+ feature_state = MU::Cloud::Azure.features(credentials: credentials).features.get(provider, feature)
884
+ changed = false
885
+ begin
886
+ if feature_state
887
+ if feature_state.properties.state == "Registering"
888
+ MU.log "Waiting for Feature #{provider}/#{feature} to finish registering", MU::NOTICE, details: feature_state.properties.state
889
+ sleep 30
890
+ elsif feature_state.properties.state == "NotRegistered"
891
+ MU.log "Registering Feature #{provider}/#{feature}", MU::NOTICE
892
+ MU::Cloud::Azure.features(credentials: credentials).features.register(provider, feature)
893
+ changed = true
894
+ sleep 30
895
+ else
896
+ MU.log "#{provider}/#{feature} registration state: #{feature_state.properties.state}", MU::DEBUG
897
+ end
898
+ feature_state = MU::Cloud::Azure.features(credentials: credentials).features.get(provider, feature)
899
+ end
900
+ end while feature_state and feature_state.properties.state != "Registered"
901
+ ensureProvider(provider, credentials: credentials, force: true) if changed
902
+ end
903
+
904
+ # END SDK STUBS
905
+
906
+ # BEGIN SDK CLIENT
907
+ private
908
+
909
+ @@authorization_api = {}
910
+ @@subscriptions_api = {}
911
+ @@subscriptions_factory_api = {}
912
+ @@compute_api = {}
913
+ @@billing_api = {}
914
+ @@apis_api = {}
915
+ @@network_api = {}
916
+ @@storage_api = {}
917
+ @@resources_api = {}
918
+ @@containers_api = {}
919
+ @@features_api = {}
920
+ @@apis_api = {}
921
+ @@marketplace_api = {}
922
+ @@service_identity_api = {}
923
+
924
+ # Generic wrapper for connections to Azure APIs
925
+ class SDKClient
926
+ @api = nil
927
+ @credentials = nil
928
+ @cred_hash = nil
929
+ @wrappers = {}
930
+
931
+ attr_reader :issuer
932
+ attr_reader :subclass
933
+ attr_reader :api
934
+
935
+ def initialize(api: "Compute", credentials: nil, profile: "Latest", subclass: nil)
936
+ subclass ||= api.sub(/s$/, '')+"Client"
937
+ @subclass = subclass
938
+ @wrapper_semaphore = Mutex.new
939
+ @wrapper_semaphore.synchronize {
940
+ @wrappers ||= {}
941
+ }
942
+
943
+ @credentials = MU::Cloud::Azure.credConfig(credentials, name_only: true)
944
+ @cred_hash = MU::Cloud::Azure.getSDKOptions(credentials)
945
+ if !@cred_hash
946
+ raise MuError, "Failed to load Azure credentials #{credentials ? credentials : "<default>"}"
947
+ end
948
+
949
+ # There seem to be multiple ways to get at clients, and different
950
+ # profiles available depending which way you do it, so... try that?
951
+ stdpath = "::Azure::#{api}::Profiles::#{profile}::Mgmt::Client"
952
+ begin
953
+ # Standard approach: get a client from a canned, approved profile
954
+ @api = Object.const_get(stdpath).new(@cred_hash)
955
+ rescue NameError => e
956
+ raise e if !@cred_hash[:client_secret]
957
+ # Weird approach: generate our own credentials object and invoke a
958
+ # client directly from a particular model profile
959
+ token_provider = MsRestAzure::ApplicationTokenProvider.new(
960
+ @cred_hash[:tenant_id],
961
+ @cred_hash[:client_id],
962
+ @cred_hash[:client_secret]
963
+ )
964
+ @cred_obj = MsRest::TokenCredentials.new(token_provider)
965
+ begin
966
+ modelpath = "::Azure::#{api}::Mgmt::#{profile}::#{@subclass}"
967
+ @api = Object.const_get(modelpath).new(@cred_obj)
968
+ rescue NameError => e
969
+ raise MuError, "Unable to locate a profile #{profile} of Azure API #{api}. I tried:\n#{stdpath}\n#{modelpath}"
970
+ end
971
+ end
972
+ end
973
+
974
+ # For method calls into the Azure API
975
+ # @param method_sym [Symbol]
976
+ # @param arguments [Array]
977
+ def method_missing(method_sym, *arguments)
978
+ aoe_orig = Thread.abort_on_exception
979
+ Thread.abort_on_exception = false
980
+ @wrapper_semaphore.synchronize {
981
+ return @wrappers[method_sym] if @wrappers[method_sym]
982
+ }
983
+ # there's a low-key race condition here, but it's harmless and I'm
984
+ # trying to pin down an odd deadlock condition on cleanup calls
985
+ if !arguments.nil? and arguments.size == 1
986
+ retval = @api.method(method_sym).call(arguments[0])
987
+ elsif !arguments.nil? and arguments.size > 0
988
+ retval = @api.method(method_sym).call(*arguments)
989
+ else
990
+ retval = @api.method(method_sym).call
991
+ end
992
+ deep_retval = ClientCallWrapper.new(retval, method_sym.to_s, self)
993
+ @wrapper_semaphore.synchronize {
994
+ @wrappers[method_sym] ||= deep_retval
995
+ }
996
+ Thread.abort_on_exception = aoe_orig
997
+ return @wrappers[method_sym]
998
+ end
999
+
1000
+ # The Azure SDK embeds several "sub-APIs" in each SDK client, and most
1001
+ # API calls are made from these second-tier objects. We add an extra
1002
+ # wrapper layer for these so that we can gracefully handle errors,
1003
+ # retries, etc.
1004
+ class ClientCallWrapper
1005
+
1006
+ def initialize(myobject, myname, parent)
1007
+ @myobject = myobject
1008
+ @myname = myname
1009
+ @parent = parent
1010
+ @parentname = parent.subclass
1011
+ end
1012
+
1013
+ # For method calls into the Azure API
1014
+ # @param method_sym [Symbol]
1015
+ # @param arguments [Array]
1016
+ def method_missing(method_sym, *arguments)
1017
+ MU.log "Calling #{@parentname}.#{@myname}.#{method_sym.to_s}", MU::DEBUG, details: arguments
1018
+ begin
1019
+ if !arguments.nil? and arguments.size == 1
1020
+ retval = @myobject.method(method_sym).call(arguments[0])
1021
+ elsif !arguments.nil? and arguments.size > 0
1022
+ retval = @myobject.method(method_sym).call(*arguments)
1023
+ else
1024
+ retval = @myobject.method(method_sym).call
1025
+ end
1026
+ rescue ::Net::ReadTimeout, ::Faraday::TimeoutError => e
1027
+ sleep 5
1028
+ retry
1029
+ rescue ::MsRestAzure::AzureOperationError, ::MsRest::HttpOperationError => e
1030
+ MU.log "Error calling #{@parent.api.class.name}.#{@myname}.#{method_sym.to_s}", MU::DEBUG, details: arguments
1031
+ begin
1032
+ parsed = JSON.parse(e.message)
1033
+ if parsed["response"] and parsed["response"]["body"]
1034
+ response = JSON.parse(parsed["response"]["body"])
1035
+ err = if response["code"] and response["message"]
1036
+ response
1037
+ elsif response["error"] and response["error"]["code"] and
1038
+ response["error"]["message"]
1039
+ response["error"]
1040
+ end
1041
+ if err
1042
+ if method_sym == :get and
1043
+ ["ResourceNotFound", "NotFound"].include?(err["code"])
1044
+ return nil
1045
+ elsif err["code"] == "AnotherOperationInProgress"
1046
+ sleep 10
1047
+ retry
1048
+ end
1049
+
1050
+ MU.log "#{@parent.api.class.name}.#{@myname}.#{method_sym.to_s} returned '"+err["code"]+"' - "+err["message"], MU::WARN, details: caller
1051
+ MU.log e.backtrace[0], MU::WARN, details: parsed
1052
+ raise MU::Cloud::Azure::APIError, err["code"]+": "+err["message"]+" (call was #{@parent.api.class.name}.#{@myname}.#{method_sym.to_s})"
1053
+ end
1054
+ end
1055
+ rescue JSON::ParserError
1056
+ end
1057
+ # MU.log e.inspect, MU::ERR, details: caller
1058
+ # MU.log e.message, MU::ERR, details: @parent.credentials
1059
+ end
1060
+
1061
+ retval
1062
+ end
1063
+
1064
+ end
1065
+
1066
+ end
1067
+ # END SDK CLIENT
1068
+ end
1069
+ end
1070
+ end