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