cloud-mu 2.1.0beta → 3.0.0beta

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