diff --git a/Berksfile b/Berksfile index 7daf25561..67f73b91b 100644 --- a/Berksfile +++ b/Berksfile @@ -42,3 +42,5 @@ cookbook 'runit', '~> 1.7' cookbook 's3fs', path: "#{cookbookPath}/s3fs" cookbook 'zipfile', '~> 0.1.0' #cookbook 'hashicorp-vault', '~> 2.5.0', git: "https://github.com/johnbellone/vault-cookbook" +cookbook 'demo', path: "#{siteCookbookPath}/demo" +cookbook 'windows', '= 3.2.0' diff --git a/Berksfile.lock b/Berksfile.lock index 2cb4a1aa4..d930a3db4 100644 --- a/Berksfile.lock +++ b/Berksfile.lock @@ -4,7 +4,10 @@ DEPENDENCIES awscli path: cookbooks/awscli build-essential (~> 8.0) + chef-vault (< 3.0.0) chef_nginx (~> 6.1.1) + demo + path: site_cookbooks/demo freebsd (~> 0.1.9) gunicorn (~> 1.1.2) logrotate (~> 1.9.2) @@ -42,11 +45,29 @@ DEPENDENCIES runit (~> 1.7) s3fs path: cookbooks/s3fs + windows (= 3.2.0) zipfile (~> 0.1.0) GRAPH apache2 (3.3.1) - apt (6.1.3) + application (5.2.0) + poise (~> 2.4) + poise-service (~> 1.0) + application_python (4.0.0) + application (~> 5.0) + poise (~> 2.0) + poise-python (~> 1.0) + poise-service (~> 1.0) + application_ruby (4.1.0) + application (~> 5.0) + poise (~> 2.0) + poise-ruby (~> 2.1) + poise-service (~> 1.0) + apt (6.1.4) + ark (3.1.0) + build-essential (>= 0.0.0) + seven_zip (>= 0.0.0) + windows (>= 0.0.0) aws (2.9.3) ohai (>= 2.1.0) awscli (0.2.1) @@ -56,8 +77,7 @@ GRAPH mingw (>= 1.1) seven_zip (>= 0.0.0) chef-sugar (3.4.0) - chef-vault (3.0.0) - compat_resource (>= 12.16.3) + chef-vault (2.1.1) chef_nginx (6.1.1) build-essential (>= 0.0.0) compat_resource (>= 12.16.3) @@ -79,15 +99,24 @@ GRAPH cpan (0.0.37) database (6.1.1) postgresql (>= 1.0.0) - dmg (4.0.0) + demo (0.3.0) + application (>= 0.0.0) + application_python (>= 0.0.0) + application_ruby (>= 0.0.0) + chef-vault (>= 0.0.0) + chef_nginx (>= 0.0.0) + git (>= 0.0.0) + mysql (>= 0.0.0) + nodejs (>= 0.0.0) + php (>= 0.0.0) + ruby_build (>= 0.0.0) dpkg_autostart (0.2.0) firewall (2.6.2) chef-sugar (>= 0.0.0) freebsd (0.1.10) - git (6.1.0) + git (8.0.0) build-essential (>= 0.0.0) - dmg (>= 0.0.0) - yum-epel (>= 0.0.0) + homebrew (>= 0.0.0) golang (1.7.0) gunicorn (1.1.6) python (>= 0.0.0) @@ -98,12 +127,12 @@ GRAPH poise-service (~> 1.1) rubyzip (~> 1.0) homebrew (4.2.0) - hostsfile (2.4.5) + hostsfile (3.0.1) java (1.50.0) apt (>= 0.0.0) homebrew (>= 0.0.0) windows (>= 0.0.0) - jenkins (5.0.3) + jenkins (5.0.4) compat_resource (>= 12.16.3) dpkg_autostart (>= 0.0.0) runit (>= 1.7) @@ -174,6 +203,7 @@ GRAPH chef-vault (>= 0.0.0) database (>= 0.0.0) java (>= 0.0.0) + mu-activedirectory (>= 0.0.0) mu-firewall (>= 0.0.0) mu-splunk (>= 0.0.0) mu-utility (>= 0.0.0) @@ -185,7 +215,7 @@ GRAPH yum-epel (>= 0.0.0) mu-utility (0.6.0) windows (>= 0.0.0) - mysql (8.4.0) + mysql (8.5.1) mysql-chef_gem (0.0.5) build-essential (>= 0.0.0) mysql (>= 0.0.0) @@ -203,19 +233,23 @@ GRAPH perl (>= 0.0.0) runit (>= 0.0.0) yum-epel (>= 0.0.0) + nodejs (4.0.0) + ark (>= 2.0.2) + build-essential (>= 0.0.0) + compat_resource (>= 12.16) nrpe (2.0.2) build-essential (>= 0.0.0) yum-epel (>= 0.0.0) - nssm (3.0.2) + nssm (4.0.0) windows (>= 0.0.0) - ohai (5.1.0) + ohai (5.2.0) openssl (7.1.0) oracle-instantclient (1.1.0) build-essential (>= 0.0.0) cpan (>= 0.0.0) php (>= 0.0.0) packagecloud (0.3.0) - perl (5.2.0) + perl (5.2.1) windows (>= 3.0) php (4.5.0) build-essential (>= 0.0.0) @@ -224,9 +258,18 @@ GRAPH poise (2.8.1) poise-archive (1.5.0) poise (~> 2.6) + poise-languages (2.1.1) + poise (~> 2.5) + poise-archive (~> 1.0) + poise-python (1.6.0) + poise (~> 2.7) + poise-languages (~> 2.0) + poise-ruby (2.3.0) + poise (~> 2.0) + poise-languages (~> 2.0) poise-service (1.5.2) poise (~> 2.0) - postfix (5.0.3) + postfix (5.1.1) postgresql (6.1.1) build-essential (>= 2.0.0) compat_resource (>= 12.16.3) @@ -234,6 +277,9 @@ GRAPH python (1.4.7) build-essential (>= 0.0.0) yum-epel (>= 0.0.0) + ruby_build (1.1.0) + git (>= 0.0.0) + yum-epel (>= 0.0.0) rubyzip (1.3.1) poise (~> 2.2) runit (1.8.0) @@ -252,11 +298,11 @@ GRAPH consul-cluster (~> 2.0) hashicorp-vault (~> 2.1) ssl_certificate (~> 1.11) - windows (3.1.1) + windows (3.2.0) ohai (>= 4.0.0) yum (3.13.0) yum-epel (2.1.2) compat_resource (>= 12.16.3) - zap (0.15.1) + zap (1.1.0) zipfile (0.1.0) zypper (0.4.0) diff --git a/bin/mu-aws-setup b/bin/mu-aws-setup index b10e62cc5..87fd21268 100755 --- a/bin/mu-aws-setup +++ b/bin/mu-aws-setup @@ -40,9 +40,10 @@ Usage: EOS opt :ip, "Attempt to configure the IP requested in the CHEF_PUBLIC_IP environment variable, or if none is set, to associate an arbitrary Elastic IP.", :require => false, :default => false, :type => :boolean opt :sg, "Attempt to configure a Security Group with appropriate permissions.", :require => false, :default => false, :type => :boolean - opt :logs, "Ensure the presence of an S3 bucket prefixed with 'Mu_Logs' for use with CloudTrails, syslog, etc.", :require => false, :default => false, :type => :boolean + opt :logs, "Ensure the presence of a cloud storage bucket for use with CloudTrails, syslog, deploy secrets, node SSL certificates, etc.", :require => false, :default => false, :type => :boolean opt :dns, "Ensure the presence of a private DNS Zone called for internal amongst Mu resources.", :require => false, :default => false, :type => :boolean opt :uploadlogs, "Push today's log files to the S3 bucket created by the -l option.", :require => false, :default => false, :type => :boolean + opt :ephemeral, "Make sure all of our instance store (ephemeral) block devices are mapped and available.", :require => false, :default => false, :type => :boolean end my_instance_id = MU::Cloud::AWS.getAWSMetaData("instance-id") @@ -52,6 +53,20 @@ instance = resp.reservations.first.instances.first preferred_ip = MU.mu_public_ip +if $opts[:ephemeral] + if instance.instance_type.match(/^(t2|m4)\./) + MU.log "t2 and m4 instance types do not have ephemeral volumes, skipping setup", MU::WARN + else +# instance.block_device_mappings.each { |dev| +# next if dev.ebs +# } + MU::Cloud::AWS.ec2.modify_instance_attribute( + instance_id: instance.instance_id, + block_device_mappings: MU::Cloud::AWS::Server.ephemeral_mappings + ) + end +end + # Create a security group, or manipulate an existing one, so that we have all # of the appropriate network holes. if $opts[:sg] @@ -186,6 +201,15 @@ if $opts[:logs] body: "#{key}" ) end + if File.exists?("#{MU.mySSLDir}/Mu_CA.pem") + MU.log "Putting the Mu Master's public SSL certificate into #{$bucketname}/Mu_CA.pem" + MU::Cloud::AWS.s3.put_object( + bucket: $bucketname, + key: "Mu_CA.pem", + body: File.read("#{MU.mySSLDir}/Mu_CA.pem"), + acl: "public-read", + ) + end # MU.log "Uploading Mu_CA.pem to #{$bucketname}" # MU::Cloud::AWS.s3.put_object( @@ -196,8 +220,8 @@ if $opts[:logs] # ) resp = MU::Cloud::AWS.s3.list_objects( - bucket: $bucketname, - prefix: "log_vol_ebs_key" + bucket: $bucketname, + prefix: "log_vol_ebs_key" ) owner = MU.structToHash(resp.contents.first.owner) diff --git a/bin/mu-configure b/bin/mu-configure index 09f277b41..0370dc0b0 100755 --- a/bin/mu-configure +++ b/bin/mu-configure @@ -38,6 +38,8 @@ $CONFIGURABLES = { "desc" => "IP address or hostname", "required" => true, "rootonly" => true, + "pattern" => /^(localhost|127\.0\.0\.1)$/, + "negate_pattern" => true, "changes" => ["389ds", "chef-server", "chefrun", "chefcerts"] }, "mu_admin_email" => { @@ -508,7 +510,12 @@ def importCurrentValues end def printVal(data) - if !data["value"].nil? + valid = true + valid = validate(data["value"], data, false) if data["value"] + if !valid + print " "+data["value"].to_s.red.on_black + print " (consider default of #{data["default"].to_s.bold})" if data["default"] + elsif !data["value"].nil? print " - "+data["value"].to_s.green.on_black elsif data["required"] print " - "+"REQUIRED".red.on_black @@ -602,35 +609,46 @@ def ask(desc) val end -def validate(newval, reqs) +def validate(newval, reqs, addnewline = true) ok = true - def validate_individual_value(newval, reqs) + def validate_individual_value(newval, reqs, addnewline) ok = true if reqs['boolean'] and newval != true and newval != false and newval != nil - puts "\nInvalid value '#{newval.bold}' (must be true or false)".light_red.on_black+"\n\n" + puts "\nInvalid value '#{newval.bold}' for #{reqs['title'].bold} (must be true or false)".light_red.on_black + puts "\n\n" if addnewline ok = false elsif reqs['pattern'] if newval.nil? - puts "\nSupplied value did not pass validation".light_red.on_black+"\n\n" + puts "\nSupplied value for #{reqs['title'].bold} did not pass validation".light_red.on_black + puts "\n\n" if addnewline ok = false - elsif !newval.match(reqs['pattern']) - puts "\nInvalid value '#{newval.bold}' (must match #{reqs['pattern']})".light_red.on_black+"\n\n" + elsif reqs['negate_pattern'] + if newval.to_s.match(reqs['pattern']) + puts "\nInvalid value '#{newval.bold}' for #{reqs['title'].bold} (must NOT match #{reqs['pattern']})".light_red.on_black + puts "\n\n" if addnewline + ok = false + end + elsif !newval.to_s.match(reqs['pattern']) + puts "\nInvalid value '#{newval.bold}' #{reqs['title'].bold} (must match #{reqs['pattern']})".light_red.on_black + puts "\n\n" if addnewline ok = false end end ok end + if reqs['array'] if !newval.is_a?(Array) - puts "\nInvalid value '#{newval.bold}' (should be an array)".light_red.on_black+"\n\n" + puts "\nInvalid value '#{newval.bold}' for #{reqs['title'].bold} (should be an array)".light_red.on_black + puts "\n\n" if addnewline ok = false else newval.each { |v| - ok = false if !validate_individual_value(v, reqs) + ok = false if !validate_individual_value(v, reqs, addnewline) } end else - ok = false if !validate_individual_value(newval, reqs) + ok = false if !validate_individual_value(newval, reqs, addnewline) end ok end @@ -638,6 +656,24 @@ end answer = nil changed = false +def entireConfigValid? + ok = true + $CONFIGURABLES.each_pair { |key, data| + next if !AMROOT and data['rootonly'] + if data.has_key?("subtree") + data["subtree"].each_pair { |subkey, subdata| + next if !AMROOT and subdata['rootonly'] + next if !data["value"] + ok = false if !validate(data["value"], data, false) + } + else + next if !data["value"] + ok = false if !validate(data["value"], data, false) + end + } + ok +end + if !$opts[:noninteractive] begin optlist = displayCurrentOpts @@ -653,7 +689,7 @@ if !$opts[:noninteractive] puts "" exit 0 end - if $MENU_MAP.has_key?(answer) + if $MENU_MAP.has_key?(answer) and !$MENU_MAP[answer].has_key?("subtree") newval = ask($MENU_MAP[answer]) if !validate(newval, $MENU_MAP[answer]) sleep 1 @@ -673,10 +709,16 @@ if !$opts[:noninteractive] elsif !["", "0", "O", "o"].include?(answer) puts "\nInvalid option '#{answer.bold}'".light_red.on_black+"\n\n" sleep 1 + else + answer = nil if !entireConfigValid? end end while answer != "0" and answer != "O" and answer != "o" +else + if !entireConfigValid? + puts "Configuration had validation errors, exiting.\nRe-invoke #{$0} to correct." + exit 1 + end end -# XXX validate overall input def set389DSCreds require 'mu' @@ -734,8 +776,8 @@ def set389DSCreds end if AMROOT -cur_chef_version = `/bin/rpm -q chef`.sub(/^chef-(\d+\.\d+\.\d+-\d+)\..*/, '\1').chomp -pref_chef_version = File.read("#{MU_BASE}/var/mu-chef-client-version").chomp + cur_chef_version = `/bin/rpm -q chef`.sub(/^chef-(\d+\.\d+\.\d+-\d+)\..*/, '\1').chomp + pref_chef_version = File.read("#{MU_BASE}/var/mu-chef-client-version").chomp if cur_chef_version != pref_chef_version puts "Updating MU-MASTER's Chef Client to '#{pref_chef_version}'" chef_installer = open("https://www.chef.io/chef/install.sh").read @@ -858,7 +900,7 @@ else end if $IN_AWS and AMROOT - system("#{MU_BASE}/lib/bin/mu-aws-setup --dns --sg --logs") + system("#{MU_BASE}/lib/bin/mu-aws-setup --dns --sg --logs --ephemeral") # XXX --ip? Do we really care? end @@ -931,8 +973,10 @@ if $MU_CFG['ldap']['type'] == "389 Directory Services" MU.log "Configuring 389 Directory Services", MU::NOTICE set389DSCreds system("chef-client -o 'recipe[mu-master::389ds]'") + exit 1 if $? != 0 MU::Master::LDAP.initLocalLDAP system("chef-client -o 'recipe[mu-master::sssd]'") + exit 1 if $? != 0 end end @@ -1020,6 +1064,8 @@ if $?.exitstatus != 0 or output.match(/is not a chef-vault/) ) end +MU.log "Regenerating documentation in /var/www/html/docs" +%x{#{MU_BASE}/lib/bin/mu-gen-docs} if $INITIALIZE MU.log "Setting initial password for admin user 'mu', for logging into Nagios and other built-in services.", MU::NOTICE diff --git a/bin/mu-gen-docs b/bin/mu-gen-docs index ef28e89da..06e54afcf 100755 --- a/bin/mu-gen-docs +++ b/bin/mu-gen-docs @@ -24,10 +24,11 @@ require 'json' require 'erb' require 'trollop' require 'json-schema' +require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb")) require 'mu' require 'yard' MU::Config.emitSchemaAsRuby MU.log "Generating YARD documentation in /var/www/html/docs (see http://#{ENV['CHEF_PUBLIC_IP']}/docs/frames.html)" File.umask(0022) -exec "cd #{MU.myRoot} && umask 0022 && env -i PATH=#{ENV['PATH']} HOME=#{ENV['HOME']} /usr/local/ruby-current/bin/yard doc modules -m markdown -o /var/www/html/docs && chcon -R -h -t httpd_sys_script_exec_t /var/www/html/" +exec "cd #{MU.myRoot} && umask 0022 && env -i PATH=#{ENV['PATH']} HOME=#{ENV['HOME']} /usr/local/ruby-current/bin/yard doc modules -m markdown -o /var/www/html/docs && chcon -R -h -t httpd_sys_script_exec_t /var/www/html/ ; /usr/local/ruby-current/bin/yard stats --list-undoc modules" diff --git a/bin/mu-node-manage b/bin/mu-node-manage index d31566367..c0317b583 100755 --- a/bin/mu-node-manage +++ b/bin/mu-node-manage @@ -31,11 +31,11 @@ Usage: opt :environment, "Operate exclusively on one nodes with a particular environment (e.g. dev, prod). Can be used in conjunction with -a or -d.", :require => false, :type => :string opt :override_chef_runlist, "An alternate runlist to pass to Chef, in chefrun mode.", :require => false, :type => :string opt :xecute, "Run a shell command on matching nodes. Overrides --mode and suppresses some informational output in favor of scriptability.", :require => false, :type => :string - opt :mode, "Action to perform on matching nodes. Valid actions: groom, chefrun, userdata, vaults", :require => false, :default => "chefrun", :type => :string + opt :mode, "Action to perform on matching nodes. Valid actions: groom, chefrun, awsmeta, vaults, certs", :require => false, :default => "chefrun", :type => :string end -if !["groom", "chefrun", "vaults", "userdata"].include?($opts[:mode]) - Trollop::die(:mode, "--mode must be one of: groom, chefrun, userdata, vaults") +if !["groom", "chefrun", "vaults", "userdata", "awsmeta", "certs"].include?($opts[:mode]) + Trollop::die(:mode, "--mode must be one of: groom, chefrun, awsmeta, vaults, certs") end if $opts[:platform] and !["linux", "windows"].include?($opts[:platform]) Trollop::die(:platform, "--platform must be one of: linux, windows") @@ -214,9 +214,30 @@ def runCommand(deploys = MU::MommaCat.listDeploys, nodes = [], cmd = "( chef-cli done = false begin MU.log "Running #{cmd} on #{nodename} (##{count})" if !print_output - output=`ssh -q #{nodename} "#{cmd}" 2>&1 < /dev/null` - if !$?.success? - if server['conf']["platform"] == "windows" and output.match(/NoMethodError: unknown property or method: `ConnectServer'/) + serverobj = mommacat.findLitterMate(type: "server", mu_name: nodename) + output = nil + exitcode = -1 + if serverobj.windows? + resp = nil + begin + shell = serverobj.getWinRMSession(0, timeout: 10, winrm_retries: 1) + resp = shell.run(cmd) + rescue MU::MuError => e + end + if !resp or resp.exitcode != 0 + MU.log "WinRM connection to #{nodename} failed, trying SSH", MU::WARN + else + output = resp.stdout + exitcode = resp.exitcode + end + end + if !output + # maybe this should use getSSHSession, for the sake of symmetry + output = `ssh -q #{nodename} "#{cmd}" 2>&1 < /dev/null` + exitcode = $?.exitstatus + end + if exitcode != 0 + if serverobj.windows? and output.match(/NoMethodError: unknown property or method: `ConnectServer'/) MU.log "#{nodename} encountered transient Windows/Chef ConnectServer error, retrying", MU::WARN elsif print_output done = true @@ -226,8 +247,9 @@ def runCommand(deploys = MU::MommaCat.listDeploys, nodes = [], cmd = "( chef-cli done = true MU.log "#{nodename} did not exit cleanly", MU::WARN, details: output.slice(-2000, 2000) end - exit $?.exitstatus if done + exit exitcode if done else + MU.log "#{nodename} complete" done = true end end until done @@ -258,7 +280,7 @@ def runCommand(deploys = MU::MommaCat.listDeploys, nodes = [], cmd = "( chef-cli end end -def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) +def updateAWSMetaData(deploys = MU::MommaCat.listDeploys, nodes = []) deploys.each { |muid| mommacat = MU::MommaCat.new(muid) @@ -275,6 +297,17 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) server["platform"] = "linux" if !server.has_key?("platform") pool_name = mommacat.getResourceName(svr_class) + if nodes.size > 0 + matched = false + nodes.each { |n| + if n.match(/^#{Regexp.quote(pool_name)}-[a-z0-9]{3}$/) + matched = true + end + } + next if !matched + end + + MU::Cloud::AWS::Server.createIAMProfile(pool_name, base_profile: server['iam_role'], extra_policies: server['iam_policies']) resp = MU::Cloud::AWS.autoscale.describe_auto_scaling_groups( auto_scaling_group_names: [pool_name] @@ -299,6 +332,7 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) "muUser" => MU.chef_user, "publicIP" => MU.mu_public_ip, "resourceName" => svr_class, + "windowsAdminName" => server['windows_admin_username'], "skipApplyUpdates" => server['skipinitialupdates'], "resourceType" => "server_pool" }, @@ -352,7 +386,8 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) next if !need_update # Put our Autoscale group onto a temporary launch config - MU::Cloud::AWS.autoscale.create_launch_configuration( + begin + MU::Cloud::AWS.autoscale.create_launch_configuration( launch_configuration_name: pool_name+"-TMP", user_data: Base64.encode64(userdata), image_id: server["basis"]["launch_config"]["ami_id"], @@ -364,7 +399,15 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) iam_instance_profile: launch.iam_instance_profile, ebs_optimized: server["basis"]["launch_config"]["ebs_optimized"], associate_public_ip_address: launch.associate_public_ip_address - ) + ) + rescue ::Aws::AutoScaling::Errors::ValidationError => e + if e.message.match(/Member must have length less than or equal to (\d+)/) + MU.log "Userdata script too long updating #{pool_name} Launch Config (#{Base64.encode64(userdata).size.to_s}/#{Regexp.last_match[1]} bytes)", MU::ERR + else + MU.log "Error updating #{pool_name} Launch Config", MU::ERR, details: e.message + end + next + end MU::Cloud::AWS.autoscale.update_auto_scaling_group( auto_scaling_group_name: pool_name, @@ -415,6 +458,9 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) server['conf']["platform"] = "linux" if !server['conf'].has_key?("platform") next if nodes.size > 0 and !nodes.include?(nodename) + rolename, cfm_role_name, cfm_prof_name, arn = MU::Cloud::AWS::Server.createIAMProfile(nodename, base_profile: server["conf"]['iam_role'], extra_policies: server["conf"]['iam_policies']) + MU::Cloud::AWS::Server.addStdPoliciesToIAMProfile(rolename) + mytype = "server" mytype = "server_pool" if server['conf'].has_key?("basis") or server['conf']['#TYPENAME'] == "ServerPool" or server['conf']["#MU_CLASS"] == "MU::Cloud::AWS::ServerPool" olduserdata = Base64.decode64(MU::Cloud::AWS.ec2(server['region']).describe_instance_attribute( @@ -431,6 +477,7 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) "muUser" => MU.chef_user, "publicIP" => MU.mu_public_ip, "resourceName" => server['conf']['name'], + "windowsAdminName" => server['conf']['windows_admin_username'], "skipApplyUpdates" => server['conf']['skipinitialupdates'], "resourceType" => mytype }, @@ -452,19 +499,47 @@ def updateUserdata(deploys = MU::MommaCat.listDeploys, nodes = []) end MU.log "Updating #{nodename} userdata (#{server["conf"]["platform"]})" - - MU::Cloud::AWS.ec2(server['region']).modify_instance_attribute( + begin + MU::Cloud::AWS.ec2(server['region']).modify_instance_attribute( instance_id: id, attribute: "userData", value: Base64.encode64(userdata) - ) + ) + rescue ::Aws::EC2::Errors::InvalidParameterValue => e + if e.message.match(/User data is limited to (\d+)/) + MU.log "Userdata script too long updating #{nodename} (#{userdata.size.to_s}/#{Regexp.last_match[1]} bytes)", MU::ERR + else + MU.log "Error replacing userData on #{nodename}", MU::ERR, details: e.message + end + end + } + } +end + +def sslCerts(deploys = MU::MommaCat.listDeploys, nodes = [], vaults_only: false) + badnodes = [] + count = 0 + deploys.each { |muid| + mommacat = MU::MommaCat.new(muid) + mommacat.listNodes.each_pair { |nodename, server| + next if server['conf'].nil? + server['conf']["platform"] = "linux" if !server['conf'].has_key?("platform") + next if nodes.size > 0 and !nodes.include?(nodename) + if server['conf'].nil? + MU.log "Failed to find config data for server #{nodename}", MU::WARN + next + end + server_obj = mommacat.findLitterMate(type: "server", mu_name: nodename) + mommacat.nodeSSLCerts(server_obj) } } end if $opts[:xecute] runCommand(do_deploys, do_nodes, $opts[:xecute], print_output: true) +elsif $opts[:mode] == "certs" + sslCerts(do_deploys, do_nodes) elsif $opts[:mode] == "groom" reGroom(do_deploys, do_nodes) elsif $opts[:mode] == "vaults" @@ -475,6 +550,7 @@ elsif $opts[:mode] == "chefrun" else runCommand(do_deploys, do_nodes) end -elsif $opts[:mode] == "userdata" - updateUserdata(do_deploys, do_nodes) +elsif $opts[:mode] == "userdata" or $opts[:mode] == "awsmeta" +# Need Google equiv and to select nodes correctly based on what cloud they're in + updateAWSMetaData(do_deploys, do_nodes) end diff --git a/bin/mu-upload-chef-artifacts b/bin/mu-upload-chef-artifacts index 0f4d24a7c..cfbb6ee92 100755 --- a/bin/mu-upload-chef-artifacts +++ b/bin/mu-upload-chef-artifacts @@ -192,10 +192,10 @@ add_berkshelf_cookbooks() echo "${GREEN}Uploading Berkshelf Chef cookbooks from ${BOLD}$repodir${NORM}" if [ "$match" == "" ];then - cd $repodir && $berks upload --no-freeze || exit 1 + cd $repodir && $berks upload --no-freeze --force || exit 1 elif [ "$berkshelf_cookbooks" != "" ];then echo "${GREEN}Matching only: ${BOLD}${berkshelf_cookbooks}${NORM}${GREEN}${NORM}" - cd $repodir && $berks upload $berkshelf_cookbooks --no-freeze 2>&1 || echo "${YELLOW}Missing cookbooks ok when using -m if they're not supposed to have been in $repodir/Berksfile${NORM}" + cd $repodir && $berks upload $berkshelf_cookbooks --no-freeze --force 2>&1 || echo "${YELLOW}Missing cookbooks ok when using -m if they're not supposed to have been in $repodir/Berksfile${NORM}" fi cd $MU_CHEF_CACHE } diff --git a/cookbooks/mu-master/attributes/default.rb b/cookbooks/mu-master/attributes/default.rb index ae3a15c32..f358c8672 100644 --- a/cookbooks/mu-master/attributes/default.rb +++ b/cookbooks/mu-master/attributes/default.rb @@ -92,3 +92,4 @@ default['application_attributes']['sshd_allow_groups'] = "#{ssh_user} mu-users" default['application_attributes']['sshd_allow_password_auth'] = true default['update_nagios_only'] = false +default['apache']['listen'] = [80, 443, 8443] diff --git a/cookbooks/mu-master/recipes/389ds.rb b/cookbooks/mu-master/recipes/389ds.rb index 230bdb7c6..989e13784 100644 --- a/cookbooks/mu-master/recipes/389ds.rb +++ b/cookbooks/mu-master/recipes/389ds.rb @@ -64,6 +64,12 @@ $CREDS[creds]['user'] = user if !$CREDS[creds]['user'] $CREDS[creds]['pw'] = pw if !$CREDS[creds]['pw'] } +directory "/var/log/dirsrv/admin-serv" do + user "nobody" + group "nobody" + mode 0770 + recursive true +end # %x{/usr/sbin/setenforce 0} execute "initialize 389 Directory Services" do diff --git a/cookbooks/mu-master/recipes/firewall-holes.rb b/cookbooks/mu-master/recipes/firewall-holes.rb index a0a1d45a6..a30c7724c 100644 --- a/cookbooks/mu-master/recipes/firewall-holes.rb +++ b/cookbooks/mu-master/recipes/firewall-holes.rb @@ -23,11 +23,16 @@ port [2260, 7443, 8443, 9443, 10514, 443, 80, 25] end -local_chef_ports = [4321, 9463, 9583, 16379, 8983, 8000, 9680, 9683, 9090, 5432, 5672] +local_chef_ports = [4321, 9463, 9583, 16379, 8983, 8000, 9680, 9683, 9090, 5432] firewall_rule "Chef Server ports on 127.0.0.1" do port local_chef_ports source "127.0.0.1/32" end +local_chef_ports_2 = [5672, 9999, 15672, 25672, 81, 111, 4369, 9463] +firewall_rule "Chef Server ports on 127.0.0.1 (2)" do + port local_chef_ports_2 + source "127.0.0.1/32" +end if node.has_key?(:local_ipv4) firewall_rule "Chef Server ports on #{node[:local_ipv4]}" do port local_chef_ports diff --git a/cookbooks/mu-master/recipes/init.rb b/cookbooks/mu-master/recipes/init.rb index 6fa2ff84b..0448ab852 100644 --- a/cookbooks/mu-master/recipes/init.rb +++ b/cookbooks/mu-master/recipes/init.rb @@ -28,10 +28,10 @@ # XXX We want to be able to override these things when invoked from chef-apply, # but, like, how? -CHEF_SERVER_VERSION="12.15.7-1" -CHEF_CLIENT_VERSION="12.20.3-1" -KNIFE_WINDOWS="1.8.0" -MU_BRANCH="master" +CHEF_SERVER_VERSION="12.16.14-1" +CHEF_CLIENT_VERSION="12.21.14-1" +KNIFE_WINDOWS="1.9.0" +MU_BRANCH="winrm_more_like_rm_windows" MU_BASE="/opt/mu" if File.read("/etc/ssh/sshd_config").match(/^AllowUsers\s+([^\s]+)(?:\s|$)/) SSH_USER = Regexp.last_match[1].chomp @@ -174,19 +174,34 @@ recursive true mode 0755 end -bash "set git default branch" do +bash "set git default branch to #{MU_BRANCH}" do cwd "#{MU_BASE}/lib" code <<-EOH git config branch.#{MU_BRANCH}.remote origin git config branch.#{MU_BRANCH}.merge refs/heads/#{MU_BRANCH} + git checkout #{MU_BRANCH} EOH action :nothing end git "#{MU_BASE}/lib" do repository "git://github.com/cloudamatic/mu.git" revision MU_BRANCH + checkout_branch MU_BRANCH + enable_checkout false not_if { ::Dir.exists?("#{MU_BASE}/lib/.git") } - notifies :run, "bash[set git default branch]", :immediately + notifies :run, "bash[set git default branch to #{MU_BRANCH}]", :immediately +end + +# Enable some git hook weirdness for Mu developers +["post-merge", "post-checkout", "post-rewrite"].each { |hook| + remote_file "#{MU_BASE}/lib/.git/hooks/#{hook}" do + source "file://#{MU_BASE}/lib/extras/git-fix-permissions-hook" + mode 0755 + end +} +remote_file "#{MU_BASE}/lib/.git/hooks/pre-commit" do + source "file://#{MU_BASE}/lib/extras/git-fix-branch-hook" + mode 0755 end directory MU_BASE+"/var" do @@ -300,14 +315,14 @@ mode 0755 end -["/usr/local/ruby-current", "/opt/chef/embedded", "/opt/opscode/embedded"].each { |rubydir| +["/usr/local/ruby-current", "/opt/chef/embedded"].each { |rubydir| gembin = rubydir+"/bin/gem" gemdir = Dir.glob("#{rubydir}/lib/ruby/gems/?.?.?/gems").last bundler_path = gembin.sub(/gem$/, "bundle") bash "fix #{rubydir} gem permissions" do code <<-EOH - find -P #{rubydir}/lib/ruby/gems/?.?.?/gems/ -type d -exec chmod go+rx {} \\; - find -P #{rubydir}/lib/ruby/gems/?.?.?/gems/ -type f -exec chmod go+r {} \\; + find -P #{rubydir}/lib/ruby/gems/?.?.?/ #{rubydir}/lib/ruby/site_ruby/ -type d -exec chmod go+rx {} \\; + find -P #{rubydir}/lib/ruby/gems/?.?.?/ #{rubydir}/lib/ruby/site_ruby/ -type f -exec chmod go+r {} \\; find -P #{rubydir}/bin -type f -exec chmod go+rx {} \\; EOH action :nothing @@ -342,22 +357,23 @@ execute "rm -rf #{gemdir}/knife-windows-#{Regexp.last_match[1]}" } - gem_package "#{rubydir} knife-windows #{KNIFE_WINDOWS} #{gembin}" do - gem_binary gembin - package_name "knife-windows" - version KNIFE_WINDOWS - notifies :restart, "service[chef-server]", :delayed if rubydir == "/opt/opscode/embedded" - # XXX notify mommacat if we're *not* in chef-apply... RUNNING_STANDALONE - end +# XXX rely on bundler to get this right for us +# gem_package "#{rubydir} knife-windows #{KNIFE_WINDOWS} #{gembin}" do +# gem_binary gembin +# package_name "knife-windows" +# version KNIFE_WINDOWS +# notifies :restart, "service[chef-server]", :delayed if rubydir == "/opt/opscode/embedded" +# # XXX notify mommacat if we're *not* in chef-apply... RUNNING_STANDALONE +# end - execute "Patch #{rubydir}'s knife-windows for Cygwin SSH bootstraps" do - cwd "#{gemdir}/knife-windows-#{KNIFE_WINDOWS}" - command "patch -p1 < #{MU_BASE}/lib/install/knife-windows-cygwin-#{KNIFE_WINDOWS}.patch" - not_if "grep -i 'locate_config_value(:cygwin)' #{gemdir}/knife-windows-#{KNIFE_WINDOWS}/lib/chef/knife/bootstrap_windows_base.rb" - notifies :restart, "service[chef-server]", :delayed if rubydir == "/opt/opscode/embedded" - only_if { ::Dir.exists?(gemdir) } +# execute "Patch #{rubydir}'s knife-windows for Cygwin SSH bootstraps" do +# cwd "#{gemdir}/knife-windows-#{KNIFE_WINDOWS}" +# command "patch -p1 < #{MU_BASE}/lib/install/knife-windows-cygwin-#{KNIFE_WINDOWS}.patch" +# not_if "grep -i 'locate_config_value(:cygwin)' #{gemdir}/knife-windows-#{KNIFE_WINDOWS}/lib/chef/knife/bootstrap_windows_base.rb" +# notifies :restart, "service[chef-server]", :delayed if rubydir == "/opt/opscode/embedded" +# only_if { ::Dir.exists?(gemdir) } # XXX notify mommacat if we're *not* in chef-apply... RUNNING_STANDALONE - end +# end end } @@ -455,9 +471,18 @@ only_if { RUNNING_STANDALONE } end +file "#{MU_BASE}/etc/mu.rc" do + content %Q{export MU_INSTALLDIR="#{MU_BASE}" + export MU_DATADIR="#{MU_BASE}/var" + export PATH="#{MU_BASE}/bin:/usr/local/ruby-current/bin:${PATH}:/opt/opscode/embedded/bin" +} + mode 0644 + action :create_if_missing +end + # Community cookbooks keep touching gems, and none of them are smart about our # default umask. We have to clean up after them every time. -["/usr/local/ruby-current", "/opt/chef/embedded", "/opt/opscode/embedded"].each { |rubydir| +["/usr/local/ruby-current", "/opt/chef/embedded"].each { |rubydir| execute "trigger permission fix in #{rubydir}" do command "ls /etc/motd > /dev/null" notifies :run, "bash[fix #{rubydir} gem permissions]", :delayed diff --git a/cookbooks/mu-master/recipes/sssd.rb b/cookbooks/mu-master/recipes/sssd.rb index dc2024826..d3695b0a3 100644 --- a/cookbooks/mu-master/recipes/sssd.rb +++ b/cookbooks/mu-master/recipes/sssd.rb @@ -63,6 +63,10 @@ notifies :reload, "service[sshd]", :delayed not_if "grep pam_sss.so /etc/pam.d/password-auth" end +directory "/var/log/sssd" do + mode 0750 + recursive true +end service "sssd" do action :nothing notifies :restart, "service[sshd]", :immediately @@ -70,6 +74,8 @@ template "/etc/sssd/sssd.conf" do source "sssd.conf.erb" mode 0600 + owner "root" + group "root" notifies :restart, "service[sssd]", :immediately variables( :base_dn => $MU_CFG['ldap']['base_dn'], diff --git a/cookbooks/mu-tools/libraries/helper.rb b/cookbooks/mu-tools/libraries/helper.rb index d08df4394..1d3b99268 100644 --- a/cookbooks/mu-tools/libraries/helper.rb +++ b/cookbooks/mu-tools/libraries/helper.rb @@ -116,7 +116,7 @@ def mommacat_request(action, arg) "mu_id" => mu_get_tag_value("MU-ID"), "mu_resource_name" => node[:service_name], "mu_resource_type" => res_type, - "mu_user" => node[:deployment][:chef_user], + "mu_user" => node[:deployment][:mu_user] || node[:deployment][:chef_user], "mu_deploy_secret" => get_deploy_secret, action => arg ) diff --git a/cookbooks/mu-tools/recipes/updates.rb b/cookbooks/mu-tools/recipes/updates.rb index 460630dbf..4ea67e524 100644 --- a/cookbooks/mu-tools/recipes/updates.rb +++ b/cookbooks/mu-tools/recipes/updates.rb @@ -48,7 +48,7 @@ recursive true end - if node[:os_updates_using_chef] + if node[:os_updates_using_chef] or node[:application_attributes][:os_updates_using_chef] powershell_script "Install Windows Updates" do # XXX Something in here throws a security error now. Whee. # Set-ExecutionPolicy RemoteSigned -Force diff --git a/cookbooks/mu-tools/recipes/windows-client.rb b/cookbooks/mu-tools/recipes/windows-client.rb index f8611600c..3c690313a 100644 --- a/cookbooks/mu-tools/recipes/windows-client.rb +++ b/cookbooks/mu-tools/recipes/windows-client.rb @@ -19,24 +19,107 @@ case node['platform'] when "windows" include_recipe 'chef-vault' + ::Chef::Recipe.send(:include, Chef::Mixin::PowershellOut) + +# remote_file "cygwin-x86_64.exe" do +# path "#{Chef::Config[:file_cache_path]}/cygwin-x86_64.exe" +# source "http://cygwin.com/setup-x86_64.exe" +# XXX guard with a version check +# end + +# XXX keep a local cache of packages... really our own damn mirror + cygwindir = "c:/bin/cygwin" +# pkgs = ["bash", "mintty", "vim", "curl", "openssl", "wget", "lynx", "openssh"] + +# powershell_script "install Cygwin" do +# code <<-EOH +# Start-Process -wait -FilePath "#{Chef::Config[:file_cache_path]}/cygwin-x86_64.exe" -ArgumentList "-q -n -l #{Chef::Config[:file_cache_path]} -L -R c:/bin/cygwin -s http://mirror.cs.vt.edu/pub/cygwin/cygwin/ -P #{pkgs.join(",")}" +# EOH +# not_if { ::File.exists?("#{cygwindir}/Cygwin.bat") } +# end + + # Be prepared to reinit installs that are missing key utilities +# file "#{cygwindir}/etc/setup/installed.db" do +# action :delete +# not_if { ::File.exists?("#{cygwindir}/bin/cygcheck.exe") } +# end + +# pkgs.each { |pkg| +# execute "install Cygwin package: #{pkg}" do +# cwd Chef::Config[:file_cache_path] +# command "#{Chef::Config[:file_cache_path]}/cygwin-x86_64.exe -f -A -q -R #{cygwindir} -s http://mirror.cs.vt.edu/pub/cygwin/cygwin/ -P #{pkg}" +# not_if "#{cygwindir}/bin/cygcheck -c #{pkg}".include? "OK" +# end +# } + + reboot "Cygwin LSA" do + action :nothing + reason "Enabling Cygwin LSA support" + end + + powershell_script "Configuring Cygwin LSA support" do + code <<-EOH + Invoke-Expression '& #{cygwindir}/bin/bash.exe --login -c "echo yes | /bin/cyglsa-config"' + EOH + not_if { + lsa_found = false + if registry_key_exists?("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa") + registry_get_values("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa").each { |val| + if val[:name] == "Authentication Packages" + lsa_found = true if val[:data].grep(/cyglsa64\.dll/) + break + end + } + end + lsa_found + } + notifies :reboot_now, "reboot[Cygwin LSA]", :immediately + end + + windows_vault = chef_vault_item(node['windows_auth_vault'], node['windows_auth_item']) + sshd_user = windows_vault[node['windows_sshd_username_field']] + sshd_password = windows_vault[node['windows_sshd_password_field']] + powershell_script "enable Cygwin sshd" do + code <<-EOH + Invoke-Expression -Debug '& #{cygwindir}/bin/bash.exe --login -c "ssh-host-config -y -c ntsec -w ''#{sshd_password}'' -u #{sshd_user}"' + Invoke-Expression -Debug '& #{cygwindir}/bin/bash.exe --login -c "sed -i.bak ''s/#.*StrictModes.*yes/StrictModes no/'' /etc/sshd_config"' + Invoke-Expression -Debug '& #{cygwindir}/bin/bash.exe --login -c "sed -i.bak ''s/#.*PasswordAuthentication.*yes/PasswordAuthentication no/'' /etc/sshd_config"' + EOH + sensitive true + not_if %Q{Get-Service "sshd"} + end + +# We probably don't have to do these things, but leaving them in a comment just +# in case. +# if((Get-WmiObject win32_computersystem).partofdomain){ +# Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkpasswd -d > /etc/passwd"' +# Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkgroup -l -d > /etc/group"' +# } else { +# Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkpasswd -l > /etc/passwd"' +# Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkgroup -l > /etc/group"' +# } +# Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "chown $sshd_svc_user /var/empty /var/log/sshd.log /etc/ssh*; chmod 755 /var/empty"' + + include_recipe 'mu-activedirectory' + ::Chef::Recipe.send(:include, Chef::Mixin::PowershellOut) + template "c:/bin/cygwin/etc/sshd_config" do source "sshd_config.erb" mode 0644 cookbook "mu-tools" + ignore_failure true end - windows_vault = chef_vault_item(node['windows_auth_vault'], node['windows_auth_item']) ec2config_user= windows_vault[node['windows_ec2config_username_field']] ec2config_password = windows_vault[node['windows_ec2config_password_field']] - sshd_user = windows_vault[node['windows_sshd_username_field']] - sshd_password = windows_vault[node['windows_sshd_password_field']] login_dom = "." if in_domain? ad_vault = chef_vault_item(node['ad']['domain_admin_vault'], node['ad']['domain_admin_item']) + login_dom = node['ad']['netbios_name'] windows_users node['ad']['computer_name'] do username ad_vault[node['ad']['domain_admin_username_field']] @@ -61,12 +144,22 @@ password ad_vault[node['ad']['domain_admin_password_field']] end - login_dom = node['ad']['netbios_name'] sshd_service "sshd" do service_username "#{node['ad']['netbios_name']}\\#{sshd_user}" username sshd_user password sshd_password end + + begin + resources('service[sshd]') + rescue Chef::Exceptions::ResourceNotFound + service "sshd" do + run_as_user login_dom+'\\'+sshd_user + run_as_password sshd_password + action [:enable, :start] + sensitive true + end + end else windows_users node['hostname'] do username node['windows_admin_username'] @@ -93,19 +186,20 @@ service_username ".\\#{sshd_user}" password sshd_password end - end# rescue NoMethodError - - begin - resources('service[sshd]') - rescue Chef::Exceptions::ResourceNotFound - service "sshd" do - run_as_user "#{login_dom}\\#{sshd_user}" - run_as_password sshd_password - action [:enable, :start] + begin + resources('service[sshd]') + rescue Chef::Exceptions::ResourceNotFound + service "Cygwin sshd as '#{sshd_user}'" do + service_name "sshd" + run_as_user ".\\"+sshd_user + run_as_password sshd_password + action [:enable, :start] + sensitive true + end end end else - Chef::Log.info("Unsupported platform #{node['platform']}") + Chef::Log.info("mu-tools::windows-client: Unsupported platform #{node['platform']}") end end diff --git a/cookbooks/mu-tools/resources/sshd_service.rb b/cookbooks/mu-tools/resources/sshd_service.rb index 29d73aa35..b305c32d5 100644 --- a/cookbooks/mu-tools/resources/sshd_service.rb +++ b/cookbooks/mu-tools/resources/sshd_service.rb @@ -10,29 +10,36 @@ action :config do converge_by("Configuring SSH service to run under #{new_resource.service_username}") do ssh_user_set = service_user_set?(new_resource.name, new_resource.service_username) + failed = false cmd = powershell_out("$sshd_service = Get-WmiObject Win32_service | Where-Object {$_.Name -eq '#{new_resource.name}'}; $sshd_service.Change($Null,$Null,$Null,$Null,$Null,$Null,'#{new_resource.service_username}','#{new_resource.password}',$Null,$Null,$Null)") - Chef::Log.error("Failed to change ssh service user #{cmd.stderr}") unless cmd.exitstatus == 0 + if cmd.exitstatus != 0 + Chef::Log.error("Failed to change ssh service user #{cmd.stderr}") + failed = true + end cmd = powershell_out("(Get-WmiObject Win32_service | Where-Object {$_.Name -eq '#{new_resource.name}'}).StartName") - Chef::Log.error("Failed to change ssh service user to #{new_resource.username}") if !(cmd.stdout =~ /#{new_resource.username}/) + if !(cmd.stdout =~ /#{new_resource.username}/) + Chef::Log.error("Failed to change ssh service user to #{new_resource.username}") + failed = true + end # if cmd.exitstatus == 0 and !ssh_user_set - unless ssh_user_set + unless ssh_user_set or failed # cmd = powershell_out("c:/bin/cygwin/bin/bash --login -c 'chown -R #{new_resource.username} /var/empty && chown #{new_resource.username} /var/log/sshd.log /etc/ssh*\'; Stop-Process -ProcessName #{new_resource.name} -force; Stop-Service #{new_resource.name} -Force; Start-Service #{new_resource.name}; sleep 5; Start-Service #{new_resource.name}") # We would much prefer to use the above because that wouldn't require another reboot, but in some cases the session dosen't get terminated from Mu. Throwing Chef::Application.fatal seems to work more reliably cmd = powershell_out("c:/bin/cygwin/bin/bash --login -c 'chown -R #{new_resource.username} /var/empty && chown #{new_resource.username} /var/log/sshd.log /etc/ssh*\'") - execute "kill ssh for reboot" do - command "Taskkill /im sshd.exe /f /t" - returns [0, 128, 1115] - action :nothing - end +# execute "kill ssh for reboot" do +# command "Taskkill /im sshd.exe /f /t" +# returns [0, 128, 1115] +# action :nothing +# end reboot "Setting Cygwin ssh user to #{new_resource.username}" do - action :reboot_now + action :request_reboot reason "Setting Cygwin ssh user to #{new_resource.username}" - notifies :run, "execute[kill ssh for reboot]", :immediately +# notifies :run, "execute[kill ssh for reboot]", :immediately end - kill_ssh +# kill_ssh end end end diff --git a/cookbooks/mu-tools/resources/windows_users.rb b/cookbooks/mu-tools/resources/windows_users.rb index a5a29ec15..50e032a59 100644 --- a/cookbooks/mu-tools/resources/windows_users.rb +++ b/cookbooks/mu-tools/resources/windows_users.rb @@ -14,6 +14,11 @@ default_action :config action :config do + + cookbook_file "c:\\Windows\\SysWOW64\\ntrights.exe" do + source "ntrights" + end + if is_domain_controller?(new_resource.computer_name) [new_resource.username, new_resource.ssh_user, new_resource.ec2config_user].each { |user| unless domain_user_exist?(user) @@ -46,10 +51,13 @@ end } - # This is a workaround because user data might re-install cygwin and use a random password that we don't know about. This is not idempotent, it just doesn't throw and error. + # This is a workaround because user data might re-install cygwin and use a random password that we don't know about. This is not idempotent, it just doesn't throw an error. + # XXX I think this has been resetting the domain sshd user's password, which + # is bad. Either that, or Cygwin has been, and this is the thing trying to + # solve that problem. script =<<-EOH Add-ADGroupMember 'Domain Admins' -Members #{new_resource.ssh_user} -PassThru - Set-ADAccountPassword -Identity #{new_resource.ssh_user} -NewPassword (ConvertTo-SecureString -AsPlainText '#{new_resource.ssh_password}' -Force) -PassThru +# Set-ADAccountPassword -Identity #{new_resource.ssh_user} -NewPassword (ConvertTo-SecureString -AsPlainText '#{new_resource.ssh_password}' -Force) -PassThru EOH converge_by("Added #{new_resource.ssh_user} to Domain Admin group and reset its password") do @@ -140,6 +148,12 @@ # powershell_out("new-gplink -name #{gpo_name} -target 'dc=#{new_resource.domain_name.gsub(".", ",dc=")}'").run_command end end + + %w{SeCreateTokenPrivilege SeTcbPrivilege SeAssignPrimaryTokenPrivilege}.each { |privilege| + batch "Grant local user #{new_resource.netbios_name}\\#{new_resource.ssh_user} #{privilege} right" do + code "C:\\Windows\\SysWOW64\\ntrights +r #{privilege} -u #{new_resource.netbios_name}\\#{new_resource.ssh_user}" + end + } end if in_domain? @@ -157,10 +171,11 @@ end } - directory 'C:/chef/cache' do - rights :full_control, "#{new_resource.netbios_name}\\#{new_resource.username}" - rights :full_control, "#{new_resource.netbios_name}\\#{new_resource.ssh_user}" - end + + directory 'C:/chef/cache' do + rights :full_control, "#{new_resource.netbios_name}\\#{new_resource.username}" + rights :full_control, "#{new_resource.netbios_name}\\#{new_resource.ssh_user}" + end execute "C:/bin/cygwin/bin/bash --login -c \"chown -R #{new_resource.username} /home/#{new_resource.username}\"" @@ -179,14 +194,19 @@ end else # We want to run ec2config as admin user so Windows userdata executes as admin, however the local admin account doesn't have Logon As a Service right. Domain privileges are set separately + cookbook_file "c:\\Windows\\SysWOW64\\ntrights.exe" do source "ntrights" end - [new_resource.ssh_user, new_resource.ec2config_user].each { |usr| + pass = if usr == new_resource.ec2config_user + new_resource.ec2config_password + elsif usr == new_resource.ssh_user + new_resource.ssh_password + end + user usr do - password new_resource.ec2config_password if usr == new_resource.ec2config_user - password new_resource.ssh_password if usr == new_resource.ssh_user + password pass end group "Administrators" do @@ -201,12 +221,21 @@ end } + # XXX user resource seems not to really be setting password, or is setting # in such a way that the user is being required to change it. Workaround. + powershell_script "Adjust local account params for #{usr}" do + code <<-EOH + (([adsi]('WinNT://./#{usr}, user')).psbase.invoke('SetPassword', '#{pass}')) + EOH + end + if usr == new_resource.ssh_user + %w{SeCreateTokenPrivilege SeTcbPrivilege SeAssignPrimaryTokenPrivilege}.each { |privilege| batch "Grant local user #{usr} logon as service right" do code "C:\\Windows\\SysWOW64\\ntrights +r #{privilege} -u #{usr}" end } + end } end diff --git a/demo/ami-generators/windows.yaml b/demo/ami-generators/win2k12.yaml similarity index 82% rename from demo/ami-generators/windows.yaml rename to demo/ami-generators/win2k12.yaml index da1d40186..fb7c6155d 100644 --- a/demo/ami-generators/windows.yaml +++ b/demo/ami-generators/win2k12.yaml @@ -4,8 +4,9 @@ - name: win2k12 platform: windows - size: m3.large + size: m4.large run_list: + - recipe[mu-tools::updates] - recipe[mu-utility::cleanup_image_helper] create_image: image_then_destroy: true diff --git a/demo/ami-generators/win2k16.yaml b/demo/ami-generators/win2k16.yaml new file mode 100644 index 000000000..d0995c567 --- /dev/null +++ b/demo/ami-generators/win2k16.yaml @@ -0,0 +1,15 @@ +--- + appname: mu + servers: + - + name: win2k16 + platform: windows + size: m4.large + run_list: + - recipe[mu-tools::updates] + - recipe[mu-utility::cleanup_image_helper] + create_image: + image_then_destroy: true + public: true + copy_to_regions: + - "#ALL" diff --git a/demo/simple-windows.yaml b/demo/simple-windows.yaml index 0b9fa1b5d..4d9441920 100644 --- a/demo/simple-windows.yaml +++ b/demo/simple-windows.yaml @@ -6,7 +6,7 @@ appname: demo servers: - name: windows platform: windows - size: m4.large + size: t2.large static_ip: assign_ip: true storage: diff --git a/extras/clean-stock-amis b/extras/clean-stock-amis new file mode 100755 index 000000000..c3f908eec --- /dev/null +++ b/extras/clean-stock-amis @@ -0,0 +1,48 @@ +#!/usr/local/ruby-current/bin/ruby +# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved +# +# Licensed under the BSD-3 license (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the root of the project or at +# +# http://egt-labs.com/mu/LICENSE.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'trollop' +require 'json' +require File.realpath(File.expand_path(File.dirname(__FILE__)+"/../bin/mu-load-config.rb")) +require 'mu' + +filters = [ + { + name: "owner-id", + values: [MU.account_number] + } +] + + +MU::Cloud::AWS.listRegions.each { | r| + images = MU::Cloud::AWS.ec2(r).describe_images( + filters: filters + [{ "name" => "state", "values" => ["available"]}] + ).images + images.each { |ami| + if (DateTime.now.to_time - DateTime.parse(ami.creation_date).to_time) > 15552000 and ami.name.match(/^MU-(PROD|DEV)/) + snaps = [] + ami.block_device_mappings.each { |dev| + if !dev.ebs.nil? + snaps << dev.ebs.snapshot_id + end + } + MU.log "Deregistering #{ami.name} (#{ami.creation_date})", MU::WARN, details: snaps + MU::Cloud::AWS.ec2(r).deregister_image(image_id: ami.image_id) + snaps.each { |snap_id| + MU::Cloud::AWS.ec2(r).delete_snapshot(snapshot_id: snap_id) + } + end + } +} diff --git a/extras/git-fix-branch-hook b/extras/git-fix-branch-hook new file mode 100755 index 000000000..f95205371 --- /dev/null +++ b/extras/git-fix-branch-hook @@ -0,0 +1,28 @@ +#!/bin/sh +# +# The name of the default branch of Mu is hardcoded in a couple of places for +# our installer to use. Mangle it to reflect the name of whatever branch is +# being committed, so that people don't have to think so hard when using dev +# branches. + +# XXX I don't like these non-qualified calls to executables, but OTOH this +# would behave in an incredibly annoying way on a system where someone stuck, +# say, git or GNU sed in a non-standard place. +if [ "`whoami`" == "root" ];then + if [ "$MU_LIBDIR" == "" ];then + MU_LIBDIR="/opt/mu/lib" + fi + cd $MU_LIBDIR + # XXX dumbly assume we're in Mu's LIBDIR in .git/hooks + branch=`git branch | grep '^*' | cut -d' ' -f2` + if ! grep "^ MU_BRANCH=\"$branch\"" install/installer > /dev/null;then + sed -i "s/^ MU_BRANCH=\".*\"/ MU_BRANCH=\"$branch\"/" install/installer + echo "Set default branch in install/installer to $branch" + git add install/installer + fi + if ! grep "^MU_BRANCH=\"$branch\"" cookbooks/mu-master/recipes/init.rb > /dev/null;then + sed -i "s/^MU_BRANCH=\".*\"/MU_BRANCH=\"$branch\"/" cookbooks/mu-master/recipes/init.rb + echo "Set default branch in cookbooks/mu-master/recipes/init.rb to $branch" + git add cookbooks/mu-master/recipes/init.rb + fi +fi diff --git a/extras/git-fix-permissions-hook b/extras/git-fix-permissions-hook index bd28dba9f..bd65356c5 100755 --- a/extras/git-fix-permissions-hook +++ b/extras/git-fix-permissions-hook @@ -4,8 +4,8 @@ if [ "`whoami`" == "root" ];then scriptpath="`dirname $0`" + # XXX dumbly assume we're in Mu's LIBDIR in .git/hooks library=1 - # assume we're in Mu's LIBDIR in .git/hooks source "`dirname $0`"/../../install/deprecated-bash-library.sh set_permissions "skip_rubies" fi diff --git a/install/cfn_create_mu_master.json b/install/cfn_create_mu_master.json.NEEDUPDATE similarity index 100% rename from install/cfn_create_mu_master.json rename to install/cfn_create_mu_master.json.NEEDUPDATE diff --git a/install/installer b/install/installer index 9477611f1..ba803e4c6 100755 --- a/install/installer +++ b/install/installer @@ -1,7 +1,11 @@ #!/bin/sh -CHEF_CLIENT_VERSION="12.20.3-1" -MU_BRANCH="master" +BOLD=`tput bold` +NORM=`tput sgr0` +CHEF_CLIENT_VERSION="12.21.14-1" +if [ "$MU_BRANCH" == "" ];then + MU_BRANCH="winrm_more_like_rm_windows" +fi # XXX All RHEL family. We can at least cover Debian-flavored hosts too, I bet. DIST_VERSION=`rpm -qa \*-release\* | grep -Ei "redhat|centos" | cut -d"-" -f3` @@ -42,11 +46,21 @@ if ! /bin/rpm -q $CHEF_CLIENT_PKG > /dev/null ;then /bin/sh /root/chef-install.sh -v $CHEF_CLIENT_VERSION fi + if [ -d /opt/mu/lib/cookbooks/mu-master/recipes ];then /opt/chef/bin/chef-apply /opt/mu/lib/cookbooks/mu-master/recipes/init.rb else + set +x + echo "" + echo "*** Installing Mu from the ${BOLD}$MU_BRANCH${NORM} branch ***" + echo "*** Hit ^C now if that's not what you intended ***" + echo "*** Prepend ${BOLD}MU_BRANCH=some_branch_name${NORM} to use another branch ***" + echo "" + sleep 10 + set -x /usr/bin/curl https://raw.githubusercontent.com/cloudamatic/mu/$MU_BRANCH/cookbooks/mu-master/recipes/init.rb > /root/mu-master-init-recipe.rb /opt/chef/bin/chef-apply /root/mu-master-init-recipe.rb fi +echo "Launching ${BOLD}mu-configure${NORM}" /opt/mu/bin/mu-configure $@ diff --git a/install/knife-windows-chef12-0.8.2.patch b/install/knife-windows-chef12-0.8.2.patch deleted file mode 100644 index 5d2d3bfb3..000000000 --- a/install/knife-windows-chef12-0.8.2.patch +++ /dev/null @@ -1,44 +0,0 @@ -diff -bBruPN knife-windows-0.8.2/lib/chef/knife/core/windows_bootstrap_context.rb /root/knife-windows-0.8.2-patched/lib/chef/knife/core/windows_bootstrap_context.rb ---- knife-windows-0.8.2/lib/chef/knife/core/windows_bootstrap_context.rb 2015-01-27 15:50:51.147154392 +0000 -+++ /root/knife-windows-0.8.2-patched/lib/chef/knife/core/windows_bootstrap_context.rb 2015-01-27 15:13:07.619991818 +0000 -@@ -76,6 +76,40 @@ - client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n} - end - -+ # We configure :verify_api_cert only when it's overridden on the CLI -+ # or when specified in the knife config. -+ if !@config[:node_verify_api_cert].nil? || knife_config.has_key?(:verify_api_cert) -+ value = @config[:node_verify_api_cert].nil? ? knife_config[:verify_api_cert] : @config[:node_verify_api_cert] -+ client_rb << %Q{verify_api_cert #{value}\n} -+ end -+ -+ # We configure :ssl_verify_mode only when it's overridden on the CLI -+ # or when specified in the knife config. -+ if @config[:node_ssl_verify_mode] || knife_config.has_key?(:ssl_verify_mode) -+ value = case @config[:node_ssl_verify_mode] -+ when "peer" -+ :verify_peer -+ when "none" -+ :verify_none -+ when nil -+ knife_config[:ssl_verify_mode] -+ else -+ nil -+ end -+ -+ if value -+ client_rb << %Q{ssl_verify_mode :#{value}\n} -+ end -+ end -+ -+ if @config[:ssl_verify_mode] -+ client_rb << %Q{ssl_verify_mode :#{knife_config[:ssl_verify_mode]}\n} -+ end -+ -+ unless trusted_certs.empty? -+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n} -+ end -+ - escape_and_echo(client_rb) - end - diff --git a/install/knife-windows-cygwin-0.8.2.patch b/install/knife-windows-cygwin-0.8.2.patch deleted file mode 100644 index 2369ca5d2..000000000 --- a/install/knife-windows-cygwin-0.8.2.patch +++ /dev/null @@ -1,71 +0,0 @@ -diff -BbruPN knife-windows-0.8.2/lib/chef/knife/bootstrap_windows_base.rb knife-windows-0.8.2-patched/lib/chef/knife/bootstrap_windows_base.rb ---- knife-windows-0.8.2/lib/chef/knife/bootstrap_windows_base.rb 2015-01-27 01:34:57.345453199 +0000 -+++ knife-windows-0.8.2-patched/lib/chef/knife/bootstrap_windows_base.rb 2015-01-27 01:32:30.582940660 +0000 -@@ -153,7 +153,12 @@ - # we have to run the remote commands in 2047 char chunks - create_bootstrap_bat_command do |command_chunk, chunk_num| - begin -- render_command_result = run_command(%Q!cmd.exe /C echo "Rendering #{bootstrap_bat_file} chunk #{chunk_num}" && #{command_chunk}!) -+ if locate_config_value(:cygwin) -+ render_command = %q!cd $TEMP && cmd.exe /C 'echo "Rendering !+bootstrap_bat_file+%q! chunk !+chunk_num.to_s+%q!" && !+command_chunk+%q!'! -+ else -+ render_command = %q!cmd.exe /C echo "Rendering !+bootstrap_bat_file+%q! chunk !+chunk_num.to_s+%q!" && !+command_chunk -+ end -+ render_command_result = run_command(render_command) - ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 - render_command_result - rescue SystemExit => e -@@ -174,8 +179,12 @@ - end - - def bootstrap_command -+ if locate_config_value(:cygwin) -+ @bootstrap_command ||= "cd $TEMP && cmd.exe /C #{bootstrap_bat_file}" -+ else - @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" - end -+ end - - def create_bootstrap_bat_command(&block) - bootstrap_bat = [] -@@ -194,8 +203,12 @@ - end - - def bootstrap_bat_file -+ if locate_config_value(:cygwin) -+ @bootstrap_bat_file ||= "\"bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" -+ else - @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" - end -+ end - - def locate_config_value(key) - key = key.to_sym -diff -BbruPN knife-windows-0.8.2/lib/chef/knife/bootstrap_windows_ssh.rb knife-windows-0.8.2-patched/lib/chef/knife/bootstrap_windows_ssh.rb ---- knife-windows-0.8.2/lib/chef/knife/bootstrap_windows_ssh.rb 2015-01-27 01:34:57.346453175 +0000 -+++ knife-windows-0.8.2-patched/lib/chef/knife/bootstrap_windows_ssh.rb 2015-01-27 01:32:30.582940660 +0000 -@@ -71,12 +71,24 @@ - :boolean => true, - :default => true - -+ option :cygwin, -+ :long => "--[no-]cygwin", -+ :short => "-c", -+ :description => "Assume that we have Cygwin (and a bash shell) at the client end.", -+ :boolean => true, -+ :default => false -+ - def run - bootstrap - end - - def run_command(command = '') - ssh = Chef::Knife::Ssh.new -+ if locate_config_value(:cygwin) -+ # Harvest crucial env variables that don't exist by default in -+ # Cygwin shells. -+ command = %q{export CYGWIN=nodosfilewarning && for __dir in /proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment;do cd "$__dir";for __var in *;do __var=`echo $__var | tr "[a-z]" "[A-Z]"` ; test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1;done;/bin/true;done && export TEMP="$SYSTEMROOT/TEMP" && export TMP="$TEMP"} + " && cd && " + command -+ end - ssh.name_args = [ server_name, command ] - ssh.config[:ssh_user] = locate_config_value(:ssh_user) - ssh.config[:ssh_password] = locate_config_value(:ssh_password) diff --git a/install/knife-windows-cygwin-1.1.4.patch b/install/knife-windows-cygwin-1.1.4.patch deleted file mode 100644 index 54712baa8..000000000 --- a/install/knife-windows-cygwin-1.1.4.patch +++ /dev/null @@ -1,142 +0,0 @@ -diff -BbruPN knife-windows-1.1.4/lib/chef/knife/bootstrap/windows-chef-client-msi.erb knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap/windows-chef-client-msi.erb ---- knife-windows-1.1.4/lib/chef/knife/bootstrap/windows-chef-client-msi.erb 2016-01-17 09:56:09.290955029 -0500 -+++ knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap/windows-chef-client-msi.erb 2016-01-21 12:35:30.051270076 -0500 -@@ -181,6 +181,17 @@ - - <%= install_chef %> - -+SET LookForFile="c:\opscode\chef\bin\chef-client.bat" -+@echo off -+ -+:CheckForFile -+IF EXIST %LookForFile% GOTO FoundIt -+c:\Windows\System32\timeout.exe /t 30 -+GOTO CheckForFile -+ -+:FoundIt -+@echo on -+ - @if ERRORLEVEL 1 ( - echo Chef-client package failed to install with status code !ERRORLEVEL!. > "&2" - echo See installation log for additional detail: %CHEF_CLIENT_MSI_LOG_PATH%. > "&2" -@@ -245,3 +256,4 @@ - @echo Starting chef to bootstrap the node... - <%= start_chef %> - -+ -diff -BbruPN knife-windows-1.1.4/lib/chef/knife/bootstrap_windows_base.rb knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap_windows_base.rb ---- knife-windows-1.1.4/lib/chef/knife/bootstrap_windows_base.rb 2016-01-17 09:56:09.290955029 -0500 -+++ knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap_windows_base.rb 2016-01-17 19:30:53.722165146 -0500 -@@ -324,7 +324,11 @@ - # we have to run the remote commands in 2047 char chunks - create_bootstrap_bat_command do |command_chunk| - begin -- render_command_result = run_command(command_chunk) -+ render_command = command_chunk -+ if locate_config_value(:cygwin) -+ render_command = %q!cd $TEMP && !+command_chunk -+ end -+ render_command_result = run_command(render_command) - ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 - render_command_result - rescue SystemExit => e -@@ -346,11 +350,20 @@ - end - - def bootstrap_command -+ if locate_config_value(:cygwin) -+ @bootstrap_command ||= "cd $TEMP && cmd.exe /C #{bootstrap_bat_file}" -+ else - @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" - end -+ @bootstrap_command -+ end - - def bootstrap_render_banner_command(chunk_num) -- "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ if locate_config_value(:cygwin) -+ return "echo 'Rendering #{bootstrap_bat_file} chunk #{chunk_num}'" -+ else -+ return "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ end - end - - def escape_windows_batch_characters(line) -@@ -363,11 +376,18 @@ - bootstrap_bat = "" - banner = bootstrap_render_banner_command(chunk_num += 1) - render_template(load_template(config[:bootstrap_template])).each_line do |line| -- escape_windows_batch_characters(line) - # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can - # confidently prefix every actual command with &&. - # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? -+ if locate_config_value(:cygwin) -+ render_line = "" -+ if !line.nil? and !line.chomp.strip.nil? -+ render_line = " && echo '#{line.chomp.strip.gsub(/'/, '\'\\\\\1\'\'')}' >> #{bootstrap_bat_file}" -+ end -+ else -+ escape_windows_batch_characters(line) - render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" -+ end - # Windows commands are limited to 8191 characters for machines running XP or higher but - # this includes the length of environment variables after they have been expanded. - # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner -@@ -394,8 +414,12 @@ - end - - def bootstrap_bat_file -+ if locate_config_value(:cygwin) -+ @bootstrap_bat_file ||= "\"bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" -+ else - @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" - end -+ end - - def warn_chef_config_secret_key - ui.info "* " * 40 -diff -BbruPN knife-windows-1.1.4/lib/chef/knife/bootstrap_windows_ssh.rb knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap_windows_ssh.rb ---- knife-windows-1.1.4/lib/chef/knife/bootstrap_windows_ssh.rb 2016-01-17 09:56:09.290955029 -0500 -+++ knife-windows-1.1.4-morepatched/lib/chef/knife/bootstrap_windows_ssh.rb 2016-01-17 17:32:33.916538468 -0500 -@@ -91,12 +91,25 @@ - :boolean => true, - :default => true - -+ option :cygwin, -+ :long => "--[no-]cygwin", -+ :short => "-c", -+ :description => "Assume that we have Cygwin (and a bash shell) at the client end.", -+ :boolean => true, -+ :default => false -+ -+ - def run - bootstrap - end - - def run_command(command = '') - ssh = Chef::Knife::Ssh.new -+ if locate_config_value(:cygwin) -+ # Harvest crucial env variables that don't exist by default in -+ # Cygwin shells. -+ command = %q{export CYGWIN=nodosfilewarning && for __dir in /proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment;do cd "$__dir";for __var in *;do __var=`echo $__var | tr "[a-z]" "[A-Z]"` ; test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1;done;/bin/true;done && export TEMP="$SYSTEMROOT/TEMP" && export TMP="$TEMP"} + " && cd && " + command -+ end - ssh.name_args = [ server_name, command ] - ssh.config[:ssh_user] = locate_config_value(:ssh_user) - ssh.config[:ssh_password] = locate_config_value(:ssh_password) -diff -BbruPN knife-windows-1.1.4/lib/chef/knife/core/windows_bootstrap_context.rb knife-windows-1.1.4-morepatched/lib/chef/knife/core/windows_bootstrap_context.rb ---- knife-windows-1.1.4/lib/chef/knife/core/windows_bootstrap_context.rb 2016-01-17 09:56:09.291955055 -0500 -+++ knife-windows-1.1.4-morepatched/lib/chef/knife/core/windows_bootstrap_context.rb 2016-01-21 11:41:32.941202376 -0500 -@@ -275,7 +275,12 @@ - url += "&pv=#{machine_os}" unless machine_os.nil? - url += "&m=#{machine_arch}" unless machine_arch.nil? - url += "&DownloadContext=#{download_context}" unless download_context.nil? -+ if !@config[:bootstrap_version].nil? and @config[:bootstrap_version] -+ require 'uri' -+ url += "&v=#{URI.escape(@config[:bootstrap_version])}" -+ else - url += latest_current_windows_chef_version_query -+ end - else - @config[:msi_url] - end diff --git a/install/knife-windows-cygwin-1.4.0.patch b/install/knife-windows-cygwin-1.4.0.patch deleted file mode 100644 index 2bddfaefc..000000000 --- a/install/knife-windows-cygwin-1.4.0.patch +++ /dev/null @@ -1,130 +0,0 @@ -diff -rupN knife-windows-1.4.0.pristine/lib/chef/knife/bootstrap_windows_base.rb knife-windows-1.4.0/lib/chef/knife/bootstrap_windows_base.rb ---- knife-windows-1.4.0.pristine/lib/chef/knife/bootstrap_windows_base.rb 2016-08-16 12:25:22.000000000 -0400 -+++ knife-windows-1.4.0/lib/chef/knife/bootstrap_windows_base.rb 2016-04-12 20:18:37.579414376 -0400 -@@ -335,7 +335,11 @@ class Chef - # we have to run the remote commands in 2047 char chunks - create_bootstrap_bat_command do |command_chunk| - begin -- render_command_result = run_command(command_chunk) -+ render_command = command_chunk -+ if locate_config_value(:cygwin) -+ render_command = %q!cd $TEMP && !+command_chunk -+ end -+ render_command_result = run_command(render_command) - ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 - render_command_result - rescue SystemExit => e -@@ -357,11 +361,20 @@ class Chef - end - - def bootstrap_command -+ if locate_config_value(:cygwin) -+ @bootstrap_command ||= "cd $TEMP && cmd.exe /C #{bootstrap_bat_file}" -+ else - @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" - end -+ @bootstrap_command -+ end - - def bootstrap_render_banner_command(chunk_num) -- "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ if locate_config_value(:cygwin) -+ return "echo 'Rendering #{bootstrap_bat_file} chunk #{chunk_num}'" -+ else -+ return "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ end - end - - def escape_windows_batch_characters(line) -@@ -374,11 +387,18 @@ class Chef - bootstrap_bat = "" - banner = bootstrap_render_banner_command(chunk_num += 1) - render_template(load_template(config[:bootstrap_template])).each_line do |line| -- escape_windows_batch_characters(line) - # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can - # confidently prefix every actual command with &&. - # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? -+ if locate_config_value(:cygwin) -+ render_line = "" -+ if !line.nil? and !line.chomp.strip.nil? -+ render_line = " && echo '#{line.chomp.strip.gsub(/'/, '\'\\\\\1\'\'')}' >> #{bootstrap_bat_file}" -+ end -+ else -+ escape_windows_batch_characters(line) - render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" -+ end - # Windows commands are limited to 8191 characters for machines running XP or higher but - # this includes the length of environment variables after they have been expanded. - # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner -@@ -405,8 +425,12 @@ class Chef - end - - def bootstrap_bat_file -+ if locate_config_value(:cygwin) -+ @bootstrap_bat_file ||= "\"bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" -+ else - @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" - end -+ end - - def warn_chef_config_secret_key - ui.info "* " * 40 -@@ -426,11 +450,14 @@ behavior will be removed and any 'encryp - # to whatever the target system is. We assume that we are only bootstrapping 1 node at a time - # so we don't need to worry about multipe responses from this command. - def set_target_architecture(bootstrap_architecture) -+ if locate_config_value(:cygwin) -+ else - session_results = relay_winrm_command("echo %PROCESSOR_ARCHITECTURE%") - if session_results.empty? || session_results[0].stdout.strip.empty? - raise "Response to 'echo %PROCESSOR_ARCHITECTURE%' command was invalid: #{session_results}" - end - current_architecture = session_results[0].stdout.strip == "X86" ? :i386 : :x86_64 -+ end - - if bootstrap_architecture.nil? - architecture = current_architecture -diff -rupN knife-windows-1.4.0.pristine/lib/chef/knife/bootstrap_windows_ssh.rb knife-windows-1.4.0/lib/chef/knife/bootstrap_windows_ssh.rb ---- knife-windows-1.4.0.pristine/lib/chef/knife/bootstrap_windows_ssh.rb 2016-08-16 12:25:22.000000000 -0400 -+++ knife-windows-1.4.0/lib/chef/knife/bootstrap_windows_ssh.rb 2016-04-12 20:18:37.580414402 -0400 -@@ -91,12 +91,24 @@ class Chef - :boolean => true, - :default => true - -+ option :cygwin, -+ :long => "--[no-]cygwin", -+ :short => "-c", -+ :description => "Assume that we have Cygwin (and a bash shell) at the client end.", -+ :boolean => true, -+ :default => false -+ - def run - bootstrap - end - - def run_command(command = '') - ssh = Chef::Knife::Ssh.new -+ if locate_config_value(:cygwin) -+ # Harvest crucial env variables that don't exist by default in -+ # Cygwin shells. -+ command = %q{export CYGWIN=nodosfilewarning && for __dir in /proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment;do cd "$__dir";for __var in *;do __var=`echo $__var | tr "[a-z]" "[A-Z]"` ; test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1;done;/bin/true;done && export TEMP="$SYSTEMROOT/TEMP" && export TMP="$TEMP"} + " && cd && " + command -+ end - ssh.name_args = [ server_name, command ] - ssh.config[:ssh_user] = locate_config_value(:ssh_user) - ssh.config[:ssh_password] = locate_config_value(:ssh_password) -diff -rupN knife-windows-1.4.0.pristine/lib/chef/knife/core/windows_bootstrap_context.rb knife-windows-1.4.0/lib/chef/knife/core/windows_bootstrap_context.rb ---- knife-windows-1.4.0.pristine/lib/chef/knife/core/windows_bootstrap_context.rb 2016-08-16 12:25:22.000000000 -0400 -+++ knife-windows-1.4.0/lib/chef/knife/core/windows_bootstrap_context.rb 2016-04-12 20:18:37.580414402 -0400 -@@ -285,7 +285,12 @@ WGET_PS - url += "&pv=#{machine_os}" unless machine_os.nil? - url += "&m=#{machine_arch}" unless machine_arch.nil? - url += "&DownloadContext=#{download_context}" unless download_context.nil? -+ if !@config[:bootstrap_version].nil? and @config[:bootstrap_version] -+ require 'uri' -+ url += "&v=#{URI.escape(@config[:bootstrap_version])}" -+ else - url += latest_current_windows_chef_version_query -+ end - else - @config[:msi_url] - end diff --git a/install/knife-windows-cygwin-1.8.0.patch b/install/knife-windows-cygwin-1.8.0.patch deleted file mode 100644 index 62cbc635c..000000000 --- a/install/knife-windows-cygwin-1.8.0.patch +++ /dev/null @@ -1,1088 +0,0 @@ -diff -rupN knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_base.rb knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_base.rb ---- knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_base.rb 2017-01-23 14:20:03.993814602 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_base.rb 2017-01-23 14:22:19.508463131 -0500 -@@ -331,7 +331,11 @@ class Chef - # we have to run the remote commands in 2047 char chunks - create_bootstrap_bat_command do |command_chunk| - begin -- render_command_result = run_command(command_chunk) -+ render_command = command_chunk -+ if locate_config_value(:cygwin) -+ render_command = %q!cd $TEMP && !+command_chunk -+ end -+ render_command_result = run_command(render_command) - ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 - render_command_result - rescue SystemExit => e -@@ -353,11 +357,20 @@ class Chef - end - - def bootstrap_command -+ if locate_config_value(:cygwin) -+ @bootstrap_command ||= "cd $TEMP && cmd.exe /C #{bootstrap_bat_file}" -+ else - @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" - end -+ @bootstrap_command -+ end - - def bootstrap_render_banner_command(chunk_num) -- "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ if locate_config_value(:cygwin) -+ return "echo 'Rendering #{bootstrap_bat_file} chunk #{chunk_num}'" -+ else -+ return "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ end - end - - def escape_windows_batch_characters(line) -@@ -370,11 +383,18 @@ class Chef - bootstrap_bat = "" - banner = bootstrap_render_banner_command(chunk_num += 1) - render_template(load_template(config[:bootstrap_template])).each_line do |line| -- escape_windows_batch_characters(line) - # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can - # confidently prefix every actual command with &&. - # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? -+ if locate_config_value(:cygwin) -+ render_line = "" -+ if !line.nil? and !line.chomp.strip.nil? -+ render_line = " && echo '#{line.chomp.strip.gsub(/'/, '\'\\\\\1\'\'')}' >> #{bootstrap_bat_file}" -+ end -+ else -+ escape_windows_batch_characters(line) - render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" -+ end - # Windows commands are limited to 8191 characters for machines running XP or higher but - # this includes the length of environment variables after they have been expanded. - # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner -@@ -401,8 +421,12 @@ class Chef - end - - def bootstrap_bat_file -+ if locate_config_value(:cygwin) -+ @bootstrap_bat_file ||= "\"bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" -+ else - @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" - end -+ end - - def warn_chef_config_secret_key - ui.info "* " * 40 -diff -rupN knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_base.rb.orig knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_base.rb.orig ---- knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_base.rb.orig 1969-12-31 19:00:00.000000000 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_base.rb.orig 2017-01-23 14:20:03.993814602 -0500 -@@ -0,0 +1,443 @@ -+# -+# Author:: Seth Chisamore () -+# Copyright:: Copyright (c) 2011-2016 Chef Software, Inc. -+# License:: Apache License, Version 2.0 -+# -+# Licensed under the Apache License, Version 2.0 (the "License"); -+# you may not use this file except in compliance with the License. -+# You may obtain a copy of the License at -+# -+# http://www.apache.org/licenses/LICENSE-2.0 -+# -+# Unless required by applicable law or agreed to in writing, software -+# distributed under the License is distributed on an "AS IS" BASIS, -+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+# See the License for the specific language governing permissions and -+# limitations under the License. -+# -+ -+require 'chef/knife' -+require 'chef/knife/bootstrap' -+require 'chef/encrypted_data_bag_item' -+require 'chef/knife/core/windows_bootstrap_context' -+require 'chef/knife/knife_windows_base' -+# Chef 11 PathHelper doesn't have #home -+#require 'chef/util/path_helper' -+ -+class Chef -+ class Knife -+ module BootstrapWindowsBase -+ -+ include Chef::Knife::KnifeWindowsBase -+ -+ # :nodoc: -+ # Would prefer to do this in a rational way, but can't be done b/c of -+ # Mixlib::CLI's design :( -+ def self.included(includer) -+ includer.class_eval do -+ -+ deps do -+ require 'readline' -+ require 'chef/json_compat' -+ end -+ -+ option :chef_node_name, -+ :short => "-N NAME", -+ :long => "--node-name NAME", -+ :description => "The Chef node name for your new node" -+ -+ option :prerelease, -+ :long => "--prerelease", -+ :description => "Install the pre-release chef gems" -+ -+ option :bootstrap_version, -+ :long => "--bootstrap-version VERSION", -+ :description => "The version of Chef to install", -+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v } -+ -+ option :bootstrap_proxy, -+ :long => "--bootstrap-proxy PROXY_URL", -+ :description => "The proxy server for the node being bootstrapped", -+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p } -+ -+ option :bootstrap_no_proxy, -+ :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]", -+ :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode", -+ :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np } -+ -+ option :bootstrap_install_command, -+ :long => "--bootstrap-install-command COMMANDS", -+ :description => "Custom command to install chef-client", -+ :proc => Proc.new { |ic| Chef::Config[:knife][:bootstrap_install_command] = ic } -+ -+ # DEPR: Remove this option in Chef 13 -+ option :distro, -+ :short => "-d DISTRO", -+ :long => "--distro DISTRO", -+ :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.", -+ :proc => Proc.new { |v| -+ Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use --bootstrap-template option instead.") -+ v -+ } -+ -+ option :bootstrap_template, -+ :short => "-t TEMPLATE", -+ :long => "--bootstrap-template TEMPLATE", -+ :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates." -+ -+ # DEPR: Remove this option in Chef 13 -+ option :template_file, -+ :long => "--template-file TEMPLATE", -+ :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.", -+ :proc => Proc.new { |v| -+ Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use --bootstrap-template option instead.") -+ v -+ } -+ -+ option :run_list, -+ :short => "-r RUN_LIST", -+ :long => "--run-list RUN_LIST", -+ :description => "Comma separated list of roles/recipes to apply", -+ :proc => lambda { |o| o.split(",") }, -+ :default => [] -+ -+ option :hint, -+ :long => "--hint HINT_NAME[=HINT_FILE]", -+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.", -+ :proc => Proc.new { |h| -+ Chef::Config[:knife][:hints] ||= Hash.new -+ name, path = h.split("=") -+ Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new -+ } -+ -+ option :first_boot_attributes, -+ :short => "-j JSON_ATTRIBS", -+ :long => "--json-attributes", -+ :description => "A JSON string to be added to the first run of chef-client", -+ :proc => lambda { |o| JSON.parse(o) }, -+ :default => nil -+ -+ option :first_boot_attributes_from_file, -+ :long => "--json-attribute-file FILE", -+ :description => "A JSON file to be used to the first run of chef-client", -+ :proc => lambda { |o| Chef::JSONCompat.parse(File.read(o)) }, -+ :default => nil -+ -+ # Mismatch between option 'encrypted_data_bag_secret' and it's long value '--secret' is by design for compatibility -+ option :encrypted_data_bag_secret, -+ :short => "-s SECRET", -+ :long => "--secret ", -+ :description => "The secret key to use to decrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config.", -+ :default => false -+ -+ # Mismatch between option 'encrypted_data_bag_secret_file' and it's long value '--secret-file' is by design for compatibility -+ option :encrypted_data_bag_secret_file, -+ :long => "--secret-file SECRET_FILE", -+ :description => "A file containing the secret key to use to encrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config." -+ -+ option :auth_timeout, -+ :long => "--auth-timeout MINUTES", -+ :description => "The maximum time in minutes to wait to for authentication over the transport to the node to succeed. The default value is 2 minutes.", -+ :default => 2 -+ -+ option :node_ssl_verify_mode, -+ :long => "--node-ssl-verify-mode [peer|none]", -+ :description => "Whether or not to verify the SSL cert for all HTTPS requests.", -+ :proc => Proc.new { |v| -+ valid_values = ["none", "peer"] -+ unless valid_values.include?(v) -+ raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}" -+ end -+ v -+ } -+ -+ option :node_verify_api_cert, -+ :long => "--[no-]node-verify-api-cert", -+ :description => "Verify the SSL cert for HTTPS requests to the Chef server API.", -+ :boolean => true -+ -+ option :msi_url, -+ :short => "-u URL", -+ :long => "--msi-url URL", -+ :description => "Location of the Chef Client MSI. The default templates will prefer to download from this location. The MSI will be downloaded from chef.io if not provided.", -+ :default => '' -+ -+ option :install_as_service, -+ :long => "--install-as-service", -+ :description => "Install chef-client as a Windows service", -+ :default => false -+ -+ option :bootstrap_vault_file, -+ :long => '--bootstrap-vault-file VAULT_FILE', -+ :description => 'A JSON file with a list of vault(s) and item(s) to be updated' -+ -+ option :bootstrap_vault_json, -+ :long => '--bootstrap-vault-json VAULT_JSON', -+ :description => 'A JSON string with the vault(s) and item(s) to be updated' -+ -+ option :bootstrap_vault_item, -+ :long => '--bootstrap-vault-item VAULT_ITEM', -+ :description => 'A single vault and item to update as "vault:item"', -+ :proc => Proc.new { |i| -+ (vault, item) = i.split(/:/) -+ Chef::Config[:knife][:bootstrap_vault_item] ||= {} -+ Chef::Config[:knife][:bootstrap_vault_item][vault] ||= [] -+ Chef::Config[:knife][:bootstrap_vault_item][vault].push(item) -+ Chef::Config[:knife][:bootstrap_vault_item] -+ } -+ -+ option :policy_name, -+ :long => "--policy-name POLICY_NAME", -+ :description => "Policyfile name to use (--policy-group must also be given)", -+ :default => nil -+ -+ option :policy_group, -+ :long => "--policy-group POLICY_GROUP", -+ :description => "Policy group name to use (--policy-name must also be given)", -+ :default => nil -+ -+ option :tags, -+ :long => "--tags TAGS", -+ :description => "Comma separated list of tags to apply to the node", -+ :proc => lambda { |o| o.split(/[\s,]+/) }, -+ :default => [] -+ end -+ end -+ -+ def default_bootstrap_template -+ "windows-chef-client-msi" -+ end -+ -+ def bootstrap_template -+ # The order here is important. We want to check if we have the new Chef 12 option is set first. -+ # Knife cloud plugins unfortunately all set a default option for the :distro so it should be at -+ # the end. -+ config[:bootstrap_template] || config[:template_file] || config[:distro] || default_bootstrap_template -+ end -+ -+ # TODO: This should go away when CHEF-2193 is fixed -+ def load_template(template=nil) -+ # Are we bootstrapping using an already shipped template? -+ -+ template = bootstrap_template -+ -+ # Use the template directly if it's a path to an actual file -+ if File.exists?(template) -+ Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}") -+ return IO.read(template).chomp -+ end -+ -+ # Otherwise search the template directories until we find the right one -+ bootstrap_files = [] -+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb") -+ bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir -+ ::Knife::Windows::PathHelper.all_homes('.chef', 'bootstrap', "#{template}.erb") { |p| bootstrap_files << p } -+ bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb")) -+ bootstrap_files.flatten! -+ -+ template = Array(bootstrap_files).find do |bootstrap_template| -+ Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}") -+ ::File.exists?(bootstrap_template) -+ end -+ -+ unless template -+ ui.info("Can not find bootstrap definition for #{config[:distro]}") -+ raise Errno::ENOENT -+ end -+ -+ Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}") -+ -+ IO.read(template).chomp -+ end -+ -+ def bootstrap_context -+ @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config) -+ end -+ -+ def load_correct_secret -+ knife_secret_file = Chef::Config[:knife][:encrypted_data_bag_secret_file] -+ knife_secret = Chef::Config[:knife][:encrypted_data_bag_secret] -+ cli_secret_file = config[:encrypted_data_bag_secret_file] -+ cli_secret = config[:encrypted_data_bag_secret] -+ -+ cli_secret_file = nil if cli_secret_file == knife_secret_file -+ cli_secret = nil if cli_secret == knife_secret -+ -+ cli_secret_file = Chef::EncryptedDataBagItem.load_secret(cli_secret_file) if cli_secret_file != nil -+ knife_secret_file = Chef::EncryptedDataBagItem.load_secret(knife_secret_file) if knife_secret_file != nil -+ -+ cli_secret_file || cli_secret || knife_secret_file || knife_secret -+ end -+ -+ def first_boot_attributes -+ config[:first_boot_attributes] || config[:first_boot_attributes_from_file] || {} -+ end -+ -+ def render_template(template=nil) -+ config[:first_boot_attributes] = first_boot_attributes -+ config[:secret] = load_correct_secret -+ Erubis::Eruby.new(template).evaluate(bootstrap_context) -+ end -+ -+ def bootstrap(proto=nil) -+ if Chef::Config[:knife][:encrypted_data_bag_secret_file] || Chef::Config[:knife][:encrypted_data_bag_secret] -+ warn_chef_config_secret_key -+ end -+ -+ set_target_architecture -+ -+ # adding respond_to? so this works with pre 12.4 chef clients -+ validate_options! if respond_to?(:validate_options!) -+ -+ @node_name = Array(@name_args).first -+ # back compat--templates may use this setting: -+ config[:server_name] = @node_name -+ -+ STDOUT.sync = STDERR.sync = true -+ -+ if Chef::VERSION.split('.').first.to_i == 11 && Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])) -+ ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.") -+ exit 1 -+ end -+ -+ if (defined?(chef_vault_handler) && chef_vault_handler.doing_chef_vault?) || -+ (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) -+ -+ unless locate_config_value(:chef_node_name) -+ ui.error("You must pass a node name with -N when bootstrapping with user credentials") -+ exit 1 -+ end -+ -+ client_builder.run -+ -+ if client_builder.respond_to?(:client) -+ chef_vault_handler.run(client_builder.client) -+ else -+ chef_vault_handler.run(node_name: config[:chef_node_name]) -+ end -+ -+ bootstrap_context.client_pem = client_builder.client_path -+ -+ else -+ ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...") -+ ui.info("Delete your validation key in order to use your user credentials instead") -+ ui.info("") -+ end -+ -+ wait_for_remote_response( config[:auth_timeout].to_i ) -+ -+ ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") -+ # create a bootstrap.bat file on the node -+ # we have to run the remote commands in 2047 char chunks -+ create_bootstrap_bat_command do |command_chunk| -+ begin -+ render_command_result = run_command(command_chunk) -+ ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 -+ render_command_result -+ rescue SystemExit => e -+ raise unless e.success? -+ end -+ end -+ -+ # execute the bootstrap.bat file -+ bootstrap_command_result = run_command(bootstrap_command) -+ ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0 -+ -+ bootstrap_command_result -+ end -+ -+ protected -+ -+ # Default implementation -- override only if required by the transport -+ def wait_for_remote_response(wait_max_minutes) -+ end -+ -+ def bootstrap_command -+ @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" -+ end -+ -+ def bootstrap_render_banner_command(chunk_num) -+ "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" -+ end -+ -+ def escape_windows_batch_characters(line) -+ # TODO: The commands are going to get redirected - do we need to escape &? -+ line.gsub!(/[(<|>)^]/).each{|m| "^#{m}"} -+ end -+ -+ def create_bootstrap_bat_command() -+ chunk_num = 0 -+ bootstrap_bat = "" -+ banner = bootstrap_render_banner_command(chunk_num += 1) -+ render_template(load_template(config[:bootstrap_template])).each_line do |line| -+ escape_windows_batch_characters(line) -+ # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can -+ # confidently prefix every actual command with &&. -+ # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? -+ render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" -+ # Windows commands are limited to 8191 characters for machines running XP or higher but -+ # this includes the length of environment variables after they have been expanded. -+ # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner -+ # and once in every command redirection), we simply guess and set the max to 5000. -+ # TODO: When a more accurate method is available, fix this. -+ if bootstrap_bat.length + render_line.length + banner.length > 5000 -+ # Can't fit it into this chunk? - flush (if necessary) and then try. -+ # Do this first because banner.length might change (e.g. due to an extra digit) and -+ # prevent a fit. -+ unless bootstrap_bat.empty? -+ yield banner + bootstrap_bat -+ bootstrap_bat = "" -+ banner = bootstrap_render_banner_command(chunk_num += 1) -+ end -+ # Will this ever fit? -+ if render_line.length + banner.length > 5000 -+ raise "Command in bootstrap template too long by #{render_line.length + banner.length - 5000} characters : #{line}" -+ end -+ end -+ bootstrap_bat << render_line -+ end -+ raise "Bootstrap template was empty! Check #{config[:bootstrap_template]}" if bootstrap_bat.empty? -+ yield banner + bootstrap_bat -+ end -+ -+ def bootstrap_bat_file -+ @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" -+ end -+ -+ def warn_chef_config_secret_key -+ ui.info "* " * 40 -+ ui.warn(<<-WARNING) -+\nSpecifying the encrypted data bag secret key using an 'encrypted_data_bag_secret' -+entry in 'knife.rb' is deprecated. Please use the '--secret' or '--secret-file' -+options of this command instead. -+ -+#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this -+behavior will be removed and any 'encrypted_data_bag_secret' entries in -+'knife.rb' will be ignored completely. -+ WARNING -+ ui.info "* " * 40 -+ end -+ -+ # We allow the user to specify the desired architecture of Chef to install or we default -+ # to whatever the target system is. -+ # This is because a user might want to install a 32bit chef client on a 64bit machine -+ def set_target_architecture -+ if Chef::Config[:knife][:architecture] -+ raise "Do not set :architecture in your knife config, use :bootstrap_architecture." -+ end -+ -+ if Chef::Config[:knife][:bootstrap_architecture] -+ bootstrap_architecture = Chef::Config[:knife][:bootstrap_architecture] -+ -+ if ![:x86_64, :i386].include?(bootstrap_architecture.to_sym) -+ raise "Valid values for the knife config :bootstrap_architecture are i386 or x86_64. Supplied value is #{bootstrap_architecture}" -+ end -+ -+ # The windows install script wants i686, not i386 -+ bootstrap_architecture = :i686 if bootstrap_architecture == :i386 -+ Chef::Config[:knife][:architecture] = bootstrap_architecture -+ end -+ end -+ end -+ end -+end -diff -rupN knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_ssh.rb knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_ssh.rb ---- knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_ssh.rb 2017-01-23 14:20:03.993814602 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_ssh.rb 2017-01-23 14:26:35.622357039 -0500 -@@ -91,6 +91,14 @@ class Chef - :boolean => true, - :default => true - -+ option :cygwin, -+ :long => "--[no-]cygwin", -+ :short => "-c", -+ :description => "Assume that we have Cygwin (and a bash shell) at the client end.", -+ :boolean => true, -+ :default => false -+ -+ - def run - validate_name_args! - bootstrap -@@ -98,6 +106,12 @@ class Chef - - def run_command(command = '') - ssh = Chef::Knife::Ssh.new -+ if locate_config_value(:cygwin) -+ # Harvest crucial env variables that don't exist by default in -+ # Cygwin shells. -+ command = %q{export CYGWIN=nodosfilewarning && for __dir in /proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment;do cd "$__dir";for __var in *;do __var=`echo $__var | tr "[a-z]" "[A-Z]"` ; test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1;done;/bin/true;done && export TEMP="$SYSTEMROOT/TEMP" && export TMP="$TEMP"} + " && cd && " + command -+ end -+ - ssh.name_args = [ server_name, command ] - ssh.config[:ssh_user] = locate_config_value(:ssh_user) - ssh.config[:ssh_password] = locate_config_value(:ssh_password) -diff -rupN knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_ssh.rb.orig knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_ssh.rb.orig ---- knife-windows-1.8.0/lib/chef/knife/bootstrap_windows_ssh.rb.orig 1969-12-31 19:00:00.000000000 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/bootstrap_windows_ssh.rb.orig 2017-01-23 14:20:03.000000000 -0500 -@@ -0,0 +1,116 @@ -+# -+# Author:: Seth Chisamore () -+# Copyright:: Copyright (c) 2011-2016 Chef Software, Inc. -+# License:: Apache License, Version 2.0 -+# -+# Licensed under the Apache License, Version 2.0 (the "License"); -+# you may not use this file except in compliance with the License. -+# You may obtain a copy of the License at -+# -+# http://www.apache.org/licenses/LICENSE-2.0 -+# -+# Unless required by applicable law or agreed to in writing, software -+# distributed under the License is distributed on an "AS IS" BASIS, -+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+# See the License for the specific language governing permissions and -+# limitations under the License. -+# -+ -+require 'chef/knife/bootstrap_windows_base' -+ -+class Chef -+ class Knife -+ class BootstrapWindowsSsh < Bootstrap -+ -+ include Chef::Knife::BootstrapWindowsBase -+ -+ deps do -+ require 'chef/knife/core/windows_bootstrap_context' -+ require 'chef/json_compat' -+ require 'tempfile' -+ require 'highline' -+ require 'net/ssh' -+ require 'net/ssh/multi' -+ Chef::Knife::Ssh.load_deps -+ end -+ -+ banner "knife bootstrap windows ssh FQDN (options)" -+ -+ option :ssh_user, -+ :short => "-x USERNAME", -+ :long => "--ssh-user USERNAME", -+ :description => "The ssh username", -+ :default => "root" -+ -+ option :ssh_password, -+ :short => "-P PASSWORD", -+ :long => "--ssh-password PASSWORD", -+ :description => "The ssh password" -+ -+ option :ssh_port, -+ :short => "-p PORT", -+ :long => "--ssh-port PORT", -+ :description => "The ssh port", -+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key.strip } -+ -+ option :ssh_gateway, -+ :short => "-G GATEWAY", -+ :long => "--ssh-gateway GATEWAY", -+ :description => "The ssh gateway", -+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key } -+ -+ option :forward_agent, -+ :short => "-A", -+ :long => "--forward-agent", -+ :description => "Enable SSH agent forwarding", -+ :boolean => true -+ -+ option :identity_file, -+ :long => "--identity-file IDENTITY_FILE", -+ :description => "The SSH identity file used for authentication. [DEPRECATED] Use --ssh-identity-file instead." -+ -+ option :ssh_identity_file, -+ :short => "-i IDENTITY_FILE", -+ :long => "--ssh-identity-file IDENTITY_FILE", -+ :description => "The SSH identity file used for authentication" -+ -+ # DEPR: Remove this option for the next release. -+ option :host_key_verification, -+ :long => "--[no-]host-key-verify", -+ :description => "Verify host key, enabled by default. [DEPRECATED] Use --host-key-verify option instead.", -+ :boolean => true, -+ :default => true, -+ :proc => Proc.new { |key| -+ Chef::Log.warn("[DEPRECATED] --host-key-verification option is deprecated. Use --host-key-verify option instead.") -+ config[:host_key_verify] = key -+ } -+ -+ option :host_key_verify, -+ :long => "--[no-]host-key-verify", -+ :description => "Verify host key, enabled by default.", -+ :boolean => true, -+ :default => true -+ -+ def run -+ validate_name_args! -+ bootstrap -+ end -+ -+ def run_command(command = '') -+ ssh = Chef::Knife::Ssh.new -+ ssh.name_args = [ server_name, command ] -+ ssh.config[:ssh_user] = locate_config_value(:ssh_user) -+ ssh.config[:ssh_password] = locate_config_value(:ssh_password) -+ ssh.config[:ssh_port] = locate_config_value(:ssh_port) -+ ssh.config[:ssh_gateway] = locate_config_value(:ssh_gateway) -+ ssh.config[:identity_file] = config[:identity_file] -+ ssh.config[:ssh_identity_file] = config[:ssh_identity_file] || config[:identity_file] -+ ssh.config[:forward_agent] = config[:forward_agent] -+ ssh.config[:manual] = true -+ ssh.config[:host_key_verify] = config[:host_key_verify] -+ ssh.run -+ end -+ -+ end -+ end -+end -Binary files knife-windows-1.8.0/lib/chef/knife/.bootstrap_windows_ssh.rb.swp and knife-windows-1.8.0.patched/lib/chef/knife/.bootstrap_windows_ssh.rb.swp differ -diff -rupN knife-windows-1.8.0/lib/chef/knife/core/windows_bootstrap_context.rb knife-windows-1.8.0.patched/lib/chef/knife/core/windows_bootstrap_context.rb ---- knife-windows-1.8.0/lib/chef/knife/core/windows_bootstrap_context.rb 2017-01-23 14:20:03.994814629 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/core/windows_bootstrap_context.rb 2017-01-23 14:22:19.508463131 -0500 -@@ -310,7 +310,12 @@ WGET_PS - url += "&pv=#{machine_os}" unless machine_os.nil? - url += "&m=#{machine_arch}" unless machine_arch.nil? - url += "&DownloadContext=#{download_context}" unless download_context.nil? -+ if !@config[:bootstrap_version].nil? and @config[:bootstrap_version] -+ require 'uri' -+ url += "&v=#{URI.escape(@config[:bootstrap_version])}" -+ else - url += latest_current_windows_chef_version_query -+ end - else - @config[:msi_url] - end -diff -rupN knife-windows-1.8.0/lib/chef/knife/core/windows_bootstrap_context.rb.orig knife-windows-1.8.0.patched/lib/chef/knife/core/windows_bootstrap_context.rb.orig ---- knife-windows-1.8.0/lib/chef/knife/core/windows_bootstrap_context.rb.orig 1969-12-31 19:00:00.000000000 -0500 -+++ knife-windows-1.8.0.patched/lib/chef/knife/core/windows_bootstrap_context.rb.orig 2017-01-23 14:20:03.994814629 -0500 -@@ -0,0 +1,397 @@ -+# -+# Author:: Seth Chisamore () -+# Copyright:: Copyright (c) 2011-2016 Chef Software, Inc. -+# License:: Apache License, Version 2.0 -+# -+# Licensed under the Apache License, Version 2.0 (the "License"); -+# you may not use this file except in compliance with the License. -+# You may obtain a copy of the License at -+# -+# http://www.apache.org/licenses/LICENSE-2.0 -+# -+# Unless required by applicable law or agreed to in writing, software -+# distributed under the License is distributed on an "AS IS" BASIS, -+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+# See the License for the specific language governing permissions and -+# limitations under the License. -+# -+ -+require 'chef/knife/core/bootstrap_context' -+# Chef::Util::PathHelper in Chef 11 is a bit juvenile still -+require 'knife-windows/path_helper' -+# require 'chef/util/path_helper' -+ -+class Chef -+ class Knife -+ module Core -+ # Instances of BootstrapContext are the context objects (i.e., +self+) for -+ # bootstrap templates. For backwards compatability, they +must+ set the -+ # following instance variables: -+ # * @config - a hash of knife's config values -+ # * @run_list - the run list for the node to boostrap -+ # -+ class WindowsBootstrapContext < BootstrapContext -+ PathHelper = ::Knife::Windows::PathHelper -+ -+ attr_accessor :client_pem -+ -+ def initialize(config, run_list, chef_config, secret=nil) -+ @config = config -+ @run_list = run_list -+ @chef_config = chef_config -+ @secret = secret -+ # Compatibility with Chef 12 and Chef 11 versions -+ begin -+ # Pass along the secret parameter for Chef 12 -+ super(config, run_list, chef_config, secret) -+ rescue ArgumentError -+ # The Chef 11 base class only has parameters for initialize -+ super(config, run_list, chef_config) -+ end -+ end -+ -+ def validation_key -+ if File.exist?(File.expand_path(@chef_config[:validation_key])) -+ IO.read(File.expand_path(@chef_config[:validation_key])) -+ else -+ false -+ end -+ end -+ -+ def secret -+ escape_and_echo(@config[:secret]) -+ end -+ -+ def trusted_certs_script -+ @trusted_certs_script ||= trusted_certs_content -+ end -+ -+ def config_content -+ client_rb = <<-CONFIG -+chef_server_url "#{@chef_config[:chef_server_url]}" -+validation_client_name "#{@chef_config[:validation_client_name]}" -+file_cache_path "c:/chef/cache" -+file_backup_path "c:/chef/backup" -+cache_options ({:path => "c:/chef/cache/checksums", :skip_expires => true}) -+ CONFIG -+ if @config[:chef_node_name] -+ client_rb << %Q{node_name "#{@config[:chef_node_name]}"\n} -+ else -+ client_rb << "# Using default node name (fqdn)\n" -+ end -+ -+ if @chef_config[:config_log_level] -+ client_rb << %Q{log_level :#{@chef_config[:config_log_level]}\n} -+ else -+ client_rb << "log_level :info\n" -+ end -+ -+ client_rb << "log_location #{get_log_location}" -+ -+ # We configure :verify_api_cert only when it's overridden on the CLI -+ # or when specified in the knife config. -+ if !@config[:node_verify_api_cert].nil? || knife_config.has_key?(:verify_api_cert) -+ value = @config[:node_verify_api_cert].nil? ? knife_config[:verify_api_cert] : @config[:node_verify_api_cert] -+ client_rb << %Q{verify_api_cert #{value}\n} -+ end -+ -+ # We configure :ssl_verify_mode only when it's overridden on the CLI -+ # or when specified in the knife config. -+ if @config[:node_ssl_verify_mode] || knife_config.has_key?(:ssl_verify_mode) -+ value = case @config[:node_ssl_verify_mode] -+ when "peer" -+ :verify_peer -+ when "none" -+ :verify_none -+ when nil -+ knife_config[:ssl_verify_mode] -+ else -+ nil -+ end -+ -+ if value -+ client_rb << %Q{ssl_verify_mode :#{value}\n} -+ end -+ end -+ -+ if @config[:ssl_verify_mode] -+ client_rb << %Q{ssl_verify_mode :#{knife_config[:ssl_verify_mode]}\n} -+ end -+ -+ if knife_config[:bootstrap_proxy] -+ client_rb << "\n" -+ client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n} -+ client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n} -+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n} if knife_config[:bootstrap_no_proxy] -+ end -+ -+ if knife_config[:bootstrap_no_proxy] -+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n} -+ end -+ -+ if @config[:secret] -+ client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n} -+ end -+ -+ unless trusted_certs_script.empty? -+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n} -+ end -+ -+ if Chef::Config[:fips] -+ client_rb << <<-CONFIG -+fips true -+chef_version = ::Chef::VERSION.split(".") -+unless chef_version[0].to_i > 12 || (chef_version[0].to_i == 12 && chef_version[1].to_i >= 8) -+ raise "FIPS Mode requested but not supported by this client" -+end -+CONFIG -+ end -+ -+ escape_and_echo(client_rb) -+ end -+ -+ def get_log_location -+ if @chef_config[:config_log_location].equal?(:win_evt) -+ %Q{:#{@chef_config[:config_log_location]}\n} -+ elsif @chef_config[:config_log_location].equal?(:syslog) -+ raise "syslog is not supported for log_location on Windows OS\n" -+ elsif (@chef_config[:config_log_location].equal?(STDOUT)) -+ "STDOUT\n" -+ elsif (@chef_config[:config_log_location].equal?(STDERR)) -+ "STDERR\n" -+ elsif @chef_config[:config_log_location].nil? || @chef_config[:config_log_location].empty? -+ "STDOUT\n" -+ elsif @chef_config[:config_log_location] -+ %Q{"#{@chef_config[:config_log_location]}"\n} -+ else -+ "STDOUT\n" -+ end -+ end -+ -+ def start_chef -+ bootstrap_environment_option = bootstrap_environment.nil? ? '' : " -E #{bootstrap_environment}" -+ start_chef = "SET \"PATH=%PATH%;C:\\ruby\\bin;C:\\opscode\\chef\\bin;C:\\opscode\\chef\\embedded\\bin\"\n" -+ start_chef << "chef-client -c c:/chef/client.rb -j c:/chef/first-boot.json#{bootstrap_environment_option}\n" -+ end -+ -+ def latest_current_windows_chef_version_query -+ installer_version_string = nil -+ if @config[:prerelease] -+ installer_version_string = "&prerelease=true" -+ else -+ chef_version_string = if knife_config[:bootstrap_version] -+ knife_config[:bootstrap_version] -+ else -+ Chef::VERSION.split(".").first -+ end -+ -+ installer_version_string = "&v=#{chef_version_string}" -+ -+ # If bootstrapping a pre-release version add the prerelease query string -+ if chef_version_string.split(".").length > 3 -+ installer_version_string << "&prerelease=true" -+ end -+ end -+ -+ installer_version_string -+ end -+ -+ def win_wget -+ # I tried my best to figure out how to properly url decode and switch / to \ -+ # but this is VBScript - so I don't really care that badly. -+ win_wget = <<-WGET -+url = WScript.Arguments.Named("url") -+path = WScript.Arguments.Named("path") -+proxy = null -+'* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all -+'* / into \. Also assume that file:/// is a local absolute path and that file:// -+'* is possibly a network file path. -+If InStr(url, "file://") = 1 Then -+url = Unescape(url) -+If InStr(url, "file:///") = 1 Then -+sourcePath = Mid(url, Len("file:///") + 1) -+Else -+sourcePath = Mid(url, Len("file:") + 1) -+End If -+sourcePath = Replace(sourcePath, "/", "\\") -+ -+Set objFSO = CreateObject("Scripting.FileSystemObject") -+If objFSO.Fileexists(path) Then objFSO.DeleteFile path -+objFSO.CopyFile sourcePath, path, true -+Set objFSO = Nothing -+ -+Else -+Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP") -+Set wshShell = CreateObject( "WScript.Shell" ) -+Set objUserVariables = wshShell.Environment("USER") -+ -+rem http proxy is optional -+rem attempt to read from HTTP_PROXY env var first -+On Error Resume Next -+ -+If NOT (objUserVariables("HTTP_PROXY") = "") Then -+proxy = objUserVariables("HTTP_PROXY") -+ -+rem fall back to named arg -+ElseIf NOT (WScript.Arguments.Named("proxy") = "") Then -+proxy = WScript.Arguments.Named("proxy") -+End If -+ -+If NOT isNull(proxy) Then -+rem setProxy method is only available on ServerXMLHTTP 6.0+ -+Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0") -+objXMLHTTP.setProxy 2, proxy -+End If -+ -+On Error Goto 0 -+ -+objXMLHTTP.open "GET", url, false -+objXMLHTTP.send() -+If objXMLHTTP.Status = 200 Then -+Set objADOStream = CreateObject("ADODB.Stream") -+objADOStream.Open -+objADOStream.Type = 1 -+objADOStream.Write objXMLHTTP.ResponseBody -+objADOStream.Position = 0 -+Set objFSO = Createobject("Scripting.FileSystemObject") -+If objFSO.Fileexists(path) Then objFSO.DeleteFile path -+Set objFSO = Nothing -+objADOStream.SaveToFile path -+objADOStream.Close -+Set objADOStream = Nothing -+End If -+Set objXMLHTTP = Nothing -+End If -+WGET -+ escape_and_echo(win_wget) -+ end -+ -+ def win_wget_ps -+ win_wget_ps = <<-WGET_PS -+param( -+ [String] $remoteUrl, -+ [String] $localPath -+) -+ -+$ProxyUrl = $env:http_proxy; -+$webClient = new-object System.Net.WebClient; -+ -+if ($ProxyUrl -ne '') { -+ $WebProxy = New-Object System.Net.WebProxy($ProxyUrl,$true) -+ $WebClient.Proxy = $WebProxy -+} -+ -+$webClient.DownloadFile($remoteUrl, $localPath); -+WGET_PS -+ -+ escape_and_echo(win_wget_ps) -+ end -+ -+ def install_chef -+ # The normal install command uses regular double quotes in -+ # the install command, so request such a string from install_command -+ install_chef = install_command('"') + "\n" + fallback_install_task_command -+ end -+ -+ def bootstrap_directory -+ bootstrap_directory = "C:\\chef" -+ end -+ -+ def local_download_path -+ local_download_path = "%TEMP%\\chef-client-latest.msi" -+ end -+ -+ def msi_url(machine_os=nil, machine_arch=nil, download_context=nil) -+ # The default msi path has a number of url query parameters - we attempt to substitute -+ # such parameters in as long as they are provided by the template. -+ -+ if @config[:msi_url].nil? || @config[:msi_url].empty? -+ url = "https://www.chef.io/chef/download?p=windows" -+ url += "&pv=#{machine_os}" unless machine_os.nil? -+ url += "&m=#{machine_arch}" unless machine_arch.nil? -+ url += "&DownloadContext=#{download_context}" unless download_context.nil? -+ url += latest_current_windows_chef_version_query -+ else -+ @config[:msi_url] -+ end -+ end -+ -+ def first_boot -+ escape_and_echo(super.to_json) -+ end -+ -+ # escape WIN BATCH special chars -+ # and prefixes each line with an -+ # echo -+ def escape_and_echo(file_contents) -+ file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1') -+ end -+ -+ private -+ -+ def install_command(executor_quote) -+ if @config[:install_as_service] -+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} ADDLOCAL=#{executor_quote}ChefClientFeature,ChefServiceFeature#{executor_quote}" -+ else -+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}" -+ end -+ end -+ -+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped -+ # This string should contain both the commands necessary to both create the files, as well as their content -+ def trusted_certs_content -+ content = "" -+ if @chef_config[:trusted_certs_dir] -+ Dir.glob(File.join(PathHelper.escape_glob_dir(@chef_config[:trusted_certs_dir]), "*.{crt,pem}")).each do |cert| -+ content << "> #{bootstrap_directory}/trusted_certs/#{File.basename(cert)} (\n" + -+ escape_and_echo(IO.read(File.expand_path(cert))) + "\n)\n" -+ end -+ end -+ content -+ end -+ -+ def fallback_install_task_command -+ # This command will be executed by schtasks.exe in the batch -+ # code below. To handle tasks that contain arguments that -+ # need to be double quoted, schtasks allows the use of single -+ # quotes that will later be converted to double quotes -+ command = install_command('\'') -+<<-EOH -+ @set MSIERRORCODE=!ERRORLEVEL! -+ @if ERRORLEVEL 1 ( -+ @echo WARNING: Failed to install Chef Client MSI package in remote context with status code !MSIERRORCODE!. -+ @echo WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614 -+ @set OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log" -+ @move "%CHEF_CLIENT_MSI_LOG_PATH%" "!OLDLOGLOCATION!" > NUL -+ @echo WARNING: Saving installation log of failure at !OLDLOGLOCATION! -+ @echo WARNING: Retrying installation with local context... -+ @schtasks /create /f /sc once /st 00:00:00 /tn chefclientbootstraptask /ru SYSTEM /rl HIGHEST /tr \"cmd /c #{command} & sleep 2 & waitfor /s %computername% /si chefclientinstalldone\" -+ -+ @if ERRORLEVEL 1 ( -+ @echo ERROR: Failed to create Chef Client installation scheduled task with status code !ERRORLEVEL! > "&2" -+ ) else ( -+ @echo Successfully created scheduled task to install Chef Client. -+ @schtasks /run /tn chefclientbootstraptask -+ @if ERRORLEVEL 1 ( -+ @echo ERROR: Failed to execut Chef Client installation scheduled task with status code !ERRORLEVEL!. > "&2" -+ ) else ( -+ @echo Successfully started Chef Client installation scheduled task. -+ @echo Waiting for installation to complete -- this may take a few minutes... -+ waitfor chefclientinstalldone /t 600 -+ if ERRORLEVEL 1 ( -+ @echo ERROR: Timed out waiting for Chef Client package to install -+ ) else ( -+ @echo Finished waiting for Chef Client package to install. -+ ) -+ @schtasks /delete /f /tn chefclientbootstraptask > NUL -+ ) -+ ) -+ ) else ( -+ @echo Successfully installed Chef Client package. -+ ) -+EOH -+ end -+ end -+ end -+ end -+end -\ No newline at end of file diff --git a/modules/Gemfile b/modules/Gemfile index e6f4b12f9..beba83a20 100644 --- a/modules/Gemfile +++ b/modules/Gemfile @@ -15,12 +15,11 @@ source "https://rubygems.org" gem 'yard' gem 'ruby-graphviz', "~> 1.2.2" -gem 'chef', "~> 12.20.3" +gem 'chef', "~> 12.21.14" gem 'chef-zero', "< 13" gem "aws-sdk-core", "~> 2.6.42" gem 'simple-password-gen' gem 'trollop', "~> 2.1.2" -gem 'knife-windows', '1.8.0' gem 'json-schema' gem 'colorize' gem 'color' @@ -37,3 +36,5 @@ gem 'nokogiri', "~> 1.6.8" gem 'molinillo', '< 0.6.0' gem 'solve', '>= 3.1.1' gem 'net-ldap' +gem 'winrm', '= 2.2.3' +gem 'knife-windows', :git => "https://github.com/eGT-Labs/knife-windows.git", :branch => "winrm_cert_auth" diff --git a/modules/Gemfile.lock b/modules/Gemfile.lock index 4fe5ed3c0..d2f94606f 100644 --- a/modules/Gemfile.lock +++ b/modules/Gemfile.lock @@ -1,12 +1,21 @@ +GIT + remote: https://github.com/eGT-Labs/knife-windows.git + revision: 0deb5ce9bba6718ebac6bb6c48a1a4b7bdc4b15b + branch: winrm_cert_auth + specs: + knife-windows (1.9.0) + winrm (~> 2.1) + winrm-elevated (~> 1.0) + GEM remote: https://rubygems.org/ specs: - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) aws-sdk-core (2.6.50) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sigv4 (1.0.1) + aws-sigv4 (1.0.2) berkshelf (5.6.5) addressable (~> 2.3, >= 2.3.4) berkshelf-api-client (>= 2.0.2, < 4.0) @@ -41,11 +50,11 @@ GEM celluloid-io (0.16.2) celluloid (>= 0.16.0) nio4r (>= 1.1.0) - chef (12.20.3) + chef (12.21.20) addressable bundler (>= 1.10) - chef-config (= 12.20.3) - chef-zero (>= 4.8) + chef-config (= 12.21.20) + chef-zero (>= 4.8, < 13) diff-lcs (~> 1.2, >= 1.2.4) erubis (~> 2.7) ffi-yajl (~> 2.2) @@ -70,7 +79,7 @@ GEM specinfra (~> 2.10) syslog-logger (~> 1.6) uuidtools (~> 2.1.5) - chef-config (12.20.3) + chef-config (12.21.20) addressable fuzzyurl mixlib-config (~> 2.0) @@ -89,7 +98,7 @@ GEM diff-lcs (1.3) erubis (2.7.0) eventmachine (1.2.5) - faraday (0.12.2) + faraday (0.13.1) multipart-post (>= 1.2, < 3) ffi (1.9.18) ffi-yajl (2.3.1) @@ -101,7 +110,7 @@ GEM builder (>= 2.1.2) hashie (3.5.6) highline (1.7.8) - hitimes (1.2.5) + hitimes (1.2.6) httpclient (2.8.3) iniparse (1.4.4) ipaddress (0.8.3) @@ -109,9 +118,6 @@ GEM json (2.1.0) json-schema (2.8.0) addressable (>= 2.4) - knife-windows (1.8.0) - winrm (~> 2.1) - winrm-elevated (~> 1.0) libyajl2 (1.2.0) little-plugger (1.1.4) logging (2.2.2) @@ -121,14 +127,13 @@ GEM minitar (0.6.1) mixlib-archive (0.4.1) mixlib-log - mixlib-authentication (1.4.1) - mixlib-log + mixlib-authentication (1.4.2) mixlib-cli (1.7.0) mixlib-config (2.2.4) mixlib-log (1.7.1) mixlib-shellout (2.3.2) molinillo (0.5.7) - multi_json (1.12.1) + multi_json (1.12.2) multipart-post (2.0.0) mysql (2.9.1) net-ldap (0.16.0) @@ -150,7 +155,7 @@ GEM nori (2.6.0) octokit (4.7.0) sawyer (~> 0.8.0, >= 0.5.3) - ohai (8.24.1) + ohai (8.25.0) chef-config (>= 12.5.0.alpha.1, < 14) ffi (~> 1.9) ffi-yajl (~> 2.2) @@ -165,7 +170,7 @@ GEM pg (0.21.0) plist (3.3.0) proxifier (1.0.3) - public_suffix (2.0.5) + public_suffix (3.0.0) rack (2.0.3) retryable (2.0.4) ridley (5.1.1) @@ -186,22 +191,22 @@ GEM retryable (~> 2.0) semverse (~> 2.0) varia_model (~> 0.6) - rspec (3.6.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) + rspec-support (~> 3.7.0) rspec-its (1.2.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.6.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) rspec_junit_formatter (0.2.3) builder (< 4) rspec-core (>= 2, < 4, != 2.12.0) @@ -213,17 +218,17 @@ GEM addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) semverse (2.0.0) - serverspec (2.39.1) + serverspec (2.41.1) multi_json rspec (~> 3.0) rspec-its - specinfra (~> 2.68) + specinfra (~> 2.72) sfl (2.3) simple-password-gen (0.1.5) solve (3.1.1) molinillo (>= 0.5) semverse (>= 1.1, < 3.0) - specinfra (2.70.1) + specinfra (2.72.0) net-scp net-ssh (>= 2.7, < 5.0) net-telnet @@ -254,7 +259,7 @@ GEM winrm-elevated (1.1.0) winrm (~> 2.0) winrm-fs (~> 1.0) - winrm-fs (1.0.1) + winrm-fs (1.1.0) erubis (~> 2.7) logging (>= 1.6.1, < 3.0) rubyzip (~> 1.1) @@ -268,13 +273,13 @@ PLATFORMS DEPENDENCIES aws-sdk-core (~> 2.6.42) berkshelf (< 6) - chef (~> 12.20.3) + chef (~> 12.21.14) chef-vault (< 3) chef-zero (< 13) color colorize json-schema - knife-windows (= 1.8.0) + knife-windows! molinillo (< 0.6.0) mysql net-ldap @@ -289,7 +294,8 @@ DEPENDENCIES solve (>= 3.1.1) thin trollop (~> 2.1.2) + winrm (= 2.2.3) yard BUNDLED WITH - 1.15.3 + 1.15.4 diff --git a/modules/mommacat.ru b/modules/mommacat.ru index 050997673..3ca6f9ea1 100644 --- a/modules/mommacat.ru +++ b/modules/mommacat.ru @@ -325,8 +325,15 @@ app = proc do |env| if req["mu_user"].nil? req["mu_user"] = "mu" end + requesttype = nil + ["mu_ssl_sign", "mu_bootstrap", "mu_windows_admin_creds", "add_volume"].each { |rt| + if req[rt] + requesttype = rt + break + end + } - MU.log "Processing request from #{env["REMOTE_ADDR"]} (MU-ID #{req["mu_id"]}, #{req["mu_resource_type"]}: #{req["mu_resource_name"]}, instance: #{req["mu_instance_id"]}, mu_ssl_sign: #{req["mu_ssl_sign"]}, mu_user #{req['mu_user']}, path #{env['REQUEST_PATH']})" + MU.log "Processing #{requesttype} request from #{env["REMOTE_ADDR"]} (MU-ID #{req["mu_id"]}, #{req["mu_resource_type"]}: #{req["mu_resource_name"]}, instance: #{req["mu_instance_id"]}, mu_user #{req['mu_user']}, path #{env['REQUEST_PATH']})" kittenpile = getKittenPile(req) if kittenpile.nil? or kittenpile.original_config.nil? or kittenpile.original_config[req["mu_resource_type"]+"s"].nil? returnval = throw500 "Couldn't find config data for #{req["mu_resource_type"]} in deploy_id #{req["mu_id"]}" @@ -350,21 +357,29 @@ app = proc do |env| # XXX We can't assume AWS anymore. What does this look like otherwise? # If this is an already-groomed instance, try to get a real object for it - instance = MU::MommaCat.findStray("AWS", "server", cloud_id: req["mu_instance_id"], region: server_cfg["region"], deploy_id: req["mu_id"], name: req["mu_resource_name"], dummy_ok: false).first + instance = MU::MommaCat.findStray("AWS", "server", cloud_id: req["mu_instance_id"], region: server_cfg["region"], deploy_id: req["mu_id"], name: req["mu_resource_name"], dummy_ok: false, calling_deploy: kittenpile).first mu_name = nil if instance.nil? # Now we're just checking for existence in the cloud provider, really MU.log "No existing groomed server found, verifying that a server with this cloud id exists" - instance = MU::Cloud::Server.find(cloud_id: req["mu_instance_id"], region: server_cfg["region"]) + instance = MU::MommaCat.findStray("AWS", "server", cloud_id: req["mu_instance_id"], region: server_cfg["region"], deploy_id: req["mu_id"], name: req["mu_resource_name"], dummy_ok: true, calling_deploy: kittenpile).first + if instance.nil? or instance.size == 0 + returnval = throw500 "Failed to find an instance with cloud id #{req["mu_instance_id"]}" + end +# XXX barf if this comes back with nonsense else mu_name = instance.mu_name MU.log "Found an existing node named #{mu_name}" end - if !req["mu_ssl_sign"].nil? - kittenpile.signSSLCert(req["mu_ssl_sign"]) + + if !req["mu_windows_admin_creds"].nil? + returnval[2] = [kittenpile.retrieveWindowsAdminCreds(instance).join(";")] + logstr = returnval[2].is_a?(Array) ? returnval[2].first.sub(/;.*/, ";*********") : returnval[2].sub(/;.*/, ";*********") + MU.log logstr, MU::NOTICE + elsif !req["mu_ssl_sign"].nil? + kittenpile.signSSLCert(req["mu_ssl_sign"], req["mu_ssl_sans"].split(/,/)) + kittenpile.signSSLCert(req["mu_ssl_sign"], req["mu_ssl_sans"].split(/,/)) elsif !req["add_volume"].nil? -puts instance.cloud_id -pp req if instance.respond_to?(:addVolume) # XXX make sure we handle mangled input safely params = JSON.parse(Base64.decode64(req["add_volume"])) @@ -376,6 +391,7 @@ pp req elsif !instance.nil? if !req["mu_bootstrap"].nil? kittenpile.groomNode(req["mu_instance_id"], req["mu_resource_name"], req["mu_resource_type"], mu_name: mu_name, sync_wait: true) + returnval[2] = ["Grooming asynchronously, check Momma Cat logs on the master for details."] else returnval = throw500 "Didn't get 'mu_bootstrap' parameter from instance id '#{req["mu_instance_id"]}'" ok = false @@ -394,6 +410,10 @@ pp req MU.purgeGlobals end end + if returnval[1] and returnval[1].has_key?("Content-Length") and + returnval[2] and returnval[2].is_a?(Array) + returnval[1]["Content-Length"] = returnval[2][0].size.to_s + end returnval end diff --git a/modules/mu-load-config.rb b/modules/mu-load-config.rb index a6c843e36..00fde10b4 100755 --- a/modules/mu-load-config.rb +++ b/modules/mu-load-config.rb @@ -112,6 +112,9 @@ def loadMuConfig(default_cfg_overrides = nil) return default_cfg.merge(global_cfg).freeze end +# Output an in-memory configuration hash to the standard config file location, +# in YAML. +# @param cfg [Hash]: The configuration to dump def saveMuConfig(cfg) home = Etc.getpwuid(Process.uid).dir username = Etc.getpwuid(Process.uid).name diff --git a/modules/mu.rb b/modules/mu.rb index f069718bf..1459a64e3 100644 --- a/modules/mu.rb +++ b/modules/mu.rb @@ -36,7 +36,7 @@ class << self; end end -if $MU_CFG['aws']['access_key'] == nil or $MU_CFG['aws']['access_key'].empty? +if !$MU_CFG or !$MU_CFG['aws'] or !$MU_CFG['aws']['access_key'] or $MU_CFG['aws']['access_key'].empty? ENV.delete('AWS_ACCESS_KEY_ID') ENV.delete('AWS_SECRET_ACCESS_KEY') Aws.config = {region: ENV['EC2_REGION']} @@ -515,7 +515,7 @@ def self.mySubnets end # The version of Chef we will install on nodes. - @@chefVersion = "12.20.3-1" + @@chefVersion = "12.21.14-1" # The version of Chef we will install on nodes. # @return [String] def self.chefVersion; diff --git a/modules/mu/cloud.rb b/modules/mu/cloud.rb index e328df8ea..b5a3ce5e8 100644 --- a/modules/mu/cloud.rb +++ b/modules/mu/cloud.rb @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +autoload :WinRM, "winrm" + module MU # Plugins under this namespace serve as interfaces to cloud providers and # other provisioning layers. @@ -720,11 +722,164 @@ def self.createRecordsFromConfig(*flags) if shortname == "Server" def windows? - %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 windows}.include?(@config['platform']) + return true if %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 windows}.include?(@config['platform']) + begin + return true if cloud_desc.respond_to?(:platform) and cloud_desc.platform == "Windows" +# XXX ^ that's AWS-speak, doesn't cover GCP or anything else; maybe we should require cloud layers to implement this so we can just call @cloudobj.windows? + rescue MU::MuError + return false + end + false + end + + # Gracefully message and attempt to accommodate the common transient errors peculiar to Windows nodes + # @param e [Exception]: The exception that we're handling + # @param retries [Integer]: The current number of retries, which we'll increment and pass back to the caller + # @param rebootable_fails [Integer]: The current number of reboot-worthy failures, which we'll increment and pass back to the caller + # @param max_retries [Integer]: Maximum number of retries to attempt; we'll raise an exception if this is exceeded + # @param reboot_on_problems [Boolean]: Whether we should try to reboot a "stuck" machine + # @param retry_interval [Integer]: How many seconds to wait before returning for another attempt + def handleWindowsFail(e, retries, rebootable_fails, max_retries: 30, reboot_on_problems: false, retry_interval: 45) + msg = "WinRM connection to https://"+@mu_name+":5986/wsman: #{e.message}, waiting #{retry_interval}s (attempt #{retries}/#{max_retries})", MU::WARN + if e.message.match(/execution expired/) and reboot_on_problems + if rebootable_fails >= 5 + MU.log "#{@mu_name} still misbehaving, forcing Stop and Start from API", MU::WARN + reboot(true) # vicious API stop/start + sleep retry_interval + rebootable_fails = 0 + else + if rebootable_fails >= 3 + MU.log "#{@mu_name} misbehaving, attempting to reboot from API", MU::WARN + reboot # graceful API restart + sleep retry_interval + end + rebootable_fails = rebootable_fails + 1 + end + end + if retries < max_retries + if retries == 1 or (retries/max_retries <= 0.5 and (retries % 3) == 0 and retries != 0) + MU.log msg, MU::NOTICE + elsif retries/max_retries > 0.5 + MU.log msg, MU::WARN, details: e.inspect + end + sleep retry_interval + retries = retries + 1 + else + raise MuError, "#{@mu_name}: #{e.inspect} trying to connect with WinRM, max_retries exceeded", e.backtrace + end + return [retries, rebootable_fails] end - # Basic setup tasks performed on a new node during its first initial ssh + def windowsRebootPending?(shell = nil) + if shell.nil? + shell = getWinRMSession(1, 30) + end +# if (Get-Item "HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/WindowsUpdate/Auto Update/RebootRequired" -EA Ignore) { exit 1 } + cmd = %Q{ + if (Get-ChildItem "HKLM:/Software/Microsoft/Windows/CurrentVersion/Component Based Servicing/RebootPending" -EA Ignore) { + echo "Component Based Servicing/RebootPending is true" + exit 1 + } + if (Get-ItemProperty "HKLM:/SYSTEM/CurrentControlSet/Control/Session Manager" -Name PendingFileRenameOperations -EA Ignore) { + echo "Control/Session Manager/PendingFileRenameOperations is true" + exit 1 + } + try { + $util = [wmiclass]"\\\\.\\root\\ccm\\clientsdk:CCM_ClientUtilities" + $status = $util.DetermineIfRebootPending() + if(($status -ne $null) -and $status.RebootPending){ + echo "WMI says RebootPending is true" + exit 1 + } + } catch { + exit 0 + } + exit 0 + } + resp = shell.run(cmd) + returnval = resp.exitcode == 0 ? false : true + shell.close + returnval + end + + # Basic setup tasks performed on a new node during its first WinRM # connection. Most of this is terrible Windows glue. + # @param shell [WinRM::Shells::Powershell]: An active Powershell session to the new node. + def initialWinRMTasks(shell) + retries = 0 + rebootable_fails = 0 + begin + if !@config['use_cloud_provider_windows_password'] + pw = @groomer.getSecret( + vault: @config['mu_name'], + item: "windows_credentials", + field: "password" + ) + win_check_for_pw = %Q{Add-Type -AssemblyName System.DirectoryServices.AccountManagement; $Creds = (New-Object System.Management.Automation.PSCredential("#{@config["windows_admin_username"]}", (ConvertTo-SecureString "#{pw}" -AsPlainText -Force)));$DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine); $DS.ValidateCredentials($Creds.GetNetworkCredential().UserName, $Creds.GetNetworkCredential().password); echo $Result} + resp = shell.run(win_check_for_pw) + if resp.stdout.chomp != "True" + win_set_pw = %Q{(([adsi]('WinNT://./#{@config["windows_admin_username"]}, user')).psbase.invoke('SetPassword', '#{pw}'))} + resp = shell.run(win_set_pw) + puts resp.stdout + MU.log "Resetting Windows host password", MU::NOTICE, details: resp.stdout + end + end + + # Install Cygwin here, because for some reason it breaks inside Chef + # XXX would love to not do this here + pkgs = ["bash", "mintty", "vim", "curl", "openssl", "wget", "lynx", "openssh"] + admin_home = "c:/bin/cygwin/home/#{@config["windows_admin_username"]}" + install_cygwin = %Q{ + If (!(Test-Path "c:/bin/cygwin/Cygwin.bat")){ + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile("http://cygwin.com/setup-x86_64.exe","$env:Temp/setup-x86_64.exe") + Start-Process -wait -FilePath $env:Temp/setup-x86_64.exe -ArgumentList "-q -n -l $env:Temp/cygwin -R c:/bin/cygwin -s http://mirror.cs.vt.edu/pub/cygwin/cygwin/ -P #{pkgs.join(',')}" + } + if(!(Test-Path #{admin_home})){ + New-Item -type directory -path #{admin_home} + } + if(!(Test-Path #{admin_home}/.ssh)){ + New-Item -type directory -path #{admin_home}/.ssh + } + if(!(Test-Path #{admin_home}/.ssh/authorized_keys)){ + New-Item #{admin_home}/.ssh/authorized_keys -type file -force -value "#{@deploy.ssh_public_key}" + } + } + resp = shell.run(install_cygwin) + if resp.exitcode != 0 + MU.log "Failed at installing Cygwin", MU::ERR, details: resp + end + + set_hostname = true + hostname = nil + if !@config['active_directory'].nil? + if @config['active_directory']['node_type'] == "domain_controller" && @config['active_directory']['domain_controller_hostname'] + hostname = @config['active_directory']['domain_controller_hostname'] + @mu_windows_name = hostname + set_hostname = true + else + # Do we have an AD specific hostname? + hostname = @mu_windows_name + set_hostname = true + end + else + hostname = @mu_windows_name + end + resp = shell.run(%Q{hostname}) + + if resp.stdout.chomp != hostname + resp = shell.run(%Q{Rename-Computer -NewName '#{hostname}' -Force -PassThru -Restart; Restart-Computer -Force}) + MU.log "Renaming Windows host to #{hostname}; this will trigger a reboot", MU::NOTICE, details: resp.stdout + end + rescue WinRM::WinRMError => e + retries, rebootable_fails = handleWindowsFail(e, retries, rebootable_fails, max_retries: 10, reboot_on_problems: true, retry_interval: 30) + retry + end + end + + + # Basic setup tasks performed on a new node during its first initial + # ssh connection. Most of this is terrible Windows glue. # @param ssh [Net::SSH::Connection::Session]: The active SSH session to the new node. def initialSSHTasks(ssh) win_env_fix = %q{echo 'export PATH="$PATH:/cygdrive/c/opscode/chef/embedded/bin"' > "$HOME/chef-client"; echo 'prev_dir="`pwd`"; for __dir in /proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment;do cd "$__dir"; for __var in `ls * | grep -v TEMP | grep -v TMP`;do __var=`echo $__var | tr "[a-z]" "[A-Z]"`; test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1; done; done; cd "$prev_dir"; /cygdrive/c/opscode/chef/bin/chef-client.bat $@' >> "$HOME/chef-client"; chmod 700 "$HOME/chef-client"; ( grep "^alias chef-client=" "$HOME/.bashrc" || echo 'alias chef-client="$HOME/chef-client"' >> "$HOME/.bashrc" ) ; ( grep "^alias mu-groom=" "$HOME/.bashrc" || echo 'alias mu-groom="powershell -File \"c:/Program Files/Amazon/Ec2ConfigService/Scripts/UserScript.ps1\""' >> "$HOME/.bashrc" )} @@ -736,9 +891,9 @@ def initialSSHTasks(ssh) if windows? and !@config['use_cloud_provider_windows_password'] # This covers both the case where we have a windows password passed from a vault and where we need to use a a random Windows Admin password generated by MU::Cloud::Server.generateWindowsPassword pw = @groomer.getSecret( - vault: @config['mu_name'], - item: "windows_credentials", - field: "password" + vault: @config['mu_name'], + item: "windows_credentials", + field: "password" ) win_check_for_pw = %Q{powershell -Command '& {Add-Type -AssemblyName System.DirectoryServices.AccountManagement; $Creds = (New-Object System.Management.Automation.PSCredential("#{@config["windows_admin_username"]}", (ConvertTo-SecureString "#{pw}" -AsPlainText -Force)));$DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine); $DS.ValidateCredentials($Creds.GetNetworkCredential().UserName, $Creds.GetNetworkCredential().password); echo $Result}'} win_set_pw = %Q{powershell -Command "& {(([adsi]('WinNT://./#{@config["windows_admin_username"]}, user')).psbase.invoke('SetPassword', '#{pw}'))}"} @@ -812,6 +967,62 @@ def initialSSHTasks(ssh) end + # Get a privileged Powershell session on the server in question, using SSL-encrypted WinRM with certificate authentication. + # @param max_retries [Integer]: + # @param retry_interval [Integer]: + # @param timeout [Integer]: + # @param winrm_retries [Integer]: + # @param reboot_on_problems [Boolean]: + def getWinRMSession(max_retries = 40, retry_interval = 60, timeout: 30, winrm_retries: 5, reboot_on_problems: false) + nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name = getSSHConfig + @mu_name ||= @config['mu_name'] + + conn = nil + shell = nil + opts = nil + # and now, a thing I really don't want to do + MU::MommaCat.addInstanceToEtcHosts(canonical_ip, @mu_name) + + # catch exceptions that circumvent our regular call stack + Thread.abort_on_exception = false + Thread.handle_interrupt(WinRM::WinRMWSManFault => :never) { + begin + Thread.handle_interrupt(WinRM::WinRMWSManFault => :immediate) { + MU.log "(Probably harmless) Caught a WinRM::WinRMWSManFault in #{Thread.current.inspect}", MU::DEBUG, details: Thread.current.backtrace + } + ensure + # Reraise something useful + end + } + + retries = 0 + rebootable_fails = 0 + begin + MU.log "Calling WinRM on #{@mu_name}", MU::DEBUG, details: opts + opts = { + endpoint: 'https://'+@mu_name+':5986/wsman', + retry_limit: winrm_retries, + no_ssl_peer_verification: true, # XXX this should not be necessary; we get 'hostname "foo" does not match the server certificate' even when it clearly does match + ca_trust_path: "#{MU.mySSLDir}/Mu_CA.pem", + transport: :ssl, + operation_timeout: timeout, + client_cert: "#{MU.mySSLDir}/#{@mu_name}-winrm.crt", + client_key: "#{MU.mySSLDir}/#{@mu_name}-winrm.key" + } + conn = WinRM::Connection.new(opts) + MU.log "WinRM connection to #{@mu_name} created", MU::DEBUG, details: conn + shell = conn.shell(:powershell) + shell.run('ipconfig') # verify that we can do something + rescue Errno::EHOSTUNREACH, Errno::ECONNREFUSED, HTTPClient::ConnectTimeoutError, OpenSSL::SSL::SSLError, SocketError, WinRM::WinRMError, Timeout::Error => e + retries, rebootable_fails = handleWindowsFail(e, retries, rebootable_fails, max_retries: max_retries, reboot_on_problems: reboot_on_problems, retry_interval: retry_interval) + retry + ensure + MU::MommaCat.removeInstanceFromEtcHosts(@mu_name) + end + + shell + end + # @param max_retries [Integer]: Number of connection attempts to make before giving up # @param retry_interval [Integer]: Number of seconds to wait between connection attempts # @return [Net::SSH::Connection::Session] diff --git a/modules/mu/clouds/aws.rb b/modules/mu/clouds/aws.rb index 229d9ad17..1f10d6264 100644 --- a/modules/mu/clouds/aws.rb +++ b/modules/mu/clouds/aws.rb @@ -38,6 +38,23 @@ def self.listAZs(region = MU.curRegion) return zones end + # Plant a Mu deploy secret into a storage bucket somewhere for so our kittens can consume it + # @param deploy_id [String]: The deploy for which we're writing the secret + # @param value [String]: The contents of the secret + def self.writeDeploySecret(deploy_id, value, name = nil) + name ||= deploy_id+"-secret" + begin + MU.log "Writing #{name} to S3 bucket #{MU.adminBucketName}" + MU::Cloud::AWS.s3(MU.myRegion).put_object( + acl: "private", + bucket: MU.adminBucketName, + key: name, + body: value + ) + rescue Aws::S3::Errors => e + raise DeployInitializeError, "Got #{e.inspect} trying to write #{name} to #{MU.adminBucketName}" + end + end # List the Amazon Web Services region names available to this account. The # region that is local to this Mu server will be listed first. diff --git a/modules/mu/clouds/aws/dnszone.rb b/modules/mu/clouds/aws/dnszone.rb index ad46d4577..7fd698d0e 100644 --- a/modules/mu/clouds/aws/dnszone.rb +++ b/modules/mu/clouds/aws/dnszone.rb @@ -248,9 +248,9 @@ def self.createRecordsFromConfig(cfg, target: nil) } } - record_threads.each { |t| - t.join - } + record_threads.each { |t| + t.join + } end # Create a Route53 health check. @@ -610,7 +610,7 @@ def self.genericMuDNSEntry(name: nil, target: nil, cloudclass: nil, noop: false, MU.log "#{dns_name} already exists", MU::DEBUG, details: e.inspect end end - return dns_name + return "#{dns_name}.platform-mu" else return nil end diff --git a/modules/mu/clouds/aws/loadbalancer.rb b/modules/mu/clouds/aws/loadbalancer.rb index 8d824ca5c..be404a24c 100644 --- a/modules/mu/clouds/aws/loadbalancer.rb +++ b/modules/mu/clouds/aws/loadbalancer.rb @@ -164,9 +164,10 @@ def create MU.log "Load Balancer is at #{lb.dns_name}" parent_thread_id = Thread.current.object_id + generic_mu_dns = nil dnsthread = Thread.new { MU.dupGlobals(parent_thread_id) - MU::Cloud::AWS::DNSZone.genericMuDNSEntry(name: @mu_name, target: "#{lb.dns_name}.", cloudclass: MU::Cloud::LoadBalancer, sync_wait: @config['dns_sync_wait']) + generic_mu_dns = MU::Cloud::AWS::DNSZone.genericMuDNSEntry(name: @mu_name, target: "#{lb.dns_name}.", cloudclass: MU::Cloud::LoadBalancer, sync_wait: @config['dns_sync_wait']) } if zones_to_try.size < @config["zones"].size @@ -521,15 +522,20 @@ def create dnsthread.join # from genericMuDNS -# XXX fix for elb2 -# if !@config['dns_records'].nil? + if !@config['dns_records'].nil? # XXX this should be a call to @deploy.nameKitten -# @config['dns_records'].each { |dnsrec| -# dnsrec['name'] = @mu_name.downcase if !dnsrec.has_key?('name') -# dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/) -# } -# MU::Cloud::AWS::DNSZone.createRecordsFromConfig(@config['dns_records'], target: resp.dns_name) -# end + @config['dns_records'].each { |dnsrec| + dnsrec['name'] = @mu_name.downcase if !dnsrec.has_key?('name') + dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/) + } + if !@config['classic'] + # XXX should be R53ALIAS, but we get "the alias target name does not lie within the target zone" + @config['dns_records'].each { |r| + r['type'] = "CNAME" + } + end + MU::Cloud::AWS::DNSZone.createRecordsFromConfig(@config['dns_records'], target: cloud_desc.dns_name) + end notify end diff --git a/modules/mu/clouds/aws/server.rb b/modules/mu/clouds/aws/server.rb index ef26e3b15..e7e1c0141 100644 --- a/modules/mu/clouds/aws/server.rb +++ b/modules/mu/clouds/aws/server.rb @@ -95,22 +95,24 @@ def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil) @config = MU::Config.manxify(kitten_cfg) @cloud_id = cloud_id - @userdata = MU::Cloud::AWS::Server.fetchUserdata( - platform: @config["platform"], - template_variables: { - "deployKey" => Base64.urlsafe_encode64(@deploy.public_key), - "deploySSHKey" => @deploy.ssh_public_key, - "muID" => MU.deploy_id, - "muUser" => MU.mu_user, - "publicIP" => MU.mu_public_ip, - "skipApplyUpdates" => @config['skipinitialupdates'], - "windowsAdminName" => @config['windows_admin_username'], - "resourceName" => @config["name"], - "resourceType" => "server", - "platform" => @config["platform"] - }, - custom_append: @config['userdata_script'] - ) + if @deploy + @userdata = MU::Cloud::AWS::Server.fetchUserdata( + platform: @config["platform"], + template_variables: { + "deployKey" => Base64.urlsafe_encode64(@deploy.public_key), + "deploySSHKey" => @deploy.ssh_public_key, + "muID" => MU.deploy_id, + "muUser" => MU.mu_user, + "publicIP" => MU.mu_public_ip, + "skipApplyUpdates" => @config['skipinitialupdates'], + "windowsAdminName" => @config['windows_admin_username'], + "resourceName" => @config["name"], + "resourceType" => "server", + "platform" => @config["platform"] + }, + custom_append: @config['userdata_script'] + ) + end @disk_devices = MU::Cloud::AWS::Server.disk_devices @ephemeral_mappings = MU::Cloud::AWS::Server.ephemeral_mappings @@ -161,7 +163,7 @@ def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: end userdata = File.read(erbfile) begin - erb = ERB.new(userdata) + erb = ERB.new(userdata, nil, "<>") script = erb.result rescue NameError => e raise MuError, "Error parsing userdata script #{erbfile} as an ERB template: #{e.inspect}" @@ -177,7 +179,7 @@ def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: MU.log "Loaded userdata script from #{custom_append['path']}" if custom_append['use_erb'] begin - erb = ERB.new(erbfile, 1) + erb = ERB.new(erbfile, 1, "<>") if custom_append['skip_std'] script = +erb.result else @@ -312,7 +314,8 @@ def self.removeIAMProfile(rolename) # Insert a Server's standard IAM role needs into an arbitrary IAM profile def self.addStdPoliciesToIAMProfile(rolename, cloudformation_data: {}, cfm_role_name: nil) policies = Hash.new - policies['Mu_Bootstrap_Secret_'+MU.deploy_id] ='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":"arn:aws:s3:::'+MU.adminBucketName+'/'+"#{MU.deploy_id}-secret"+'"}]}' + objs = ["#{MU.deploy_id}-secret", "#{rolename}.pfx", "#{rolename}.crt", "#{rolename}.key", "#{rolename}-winrm.crt", "#{rolename}-winrm.key"] + policies['Mu_Secrets_'+MU.deploy_id] ='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":['+objs.map { |m| '"arn:aws:s3:::'+MU.adminBucketName+'/'+m+'"' }.join(",")+']}]}' policies.each_pair { |name, doc| if cloudformation_data.size > 0 if !cfm_role_name.nil? @@ -320,11 +323,11 @@ def self.addStdPoliciesToIAMProfile(rolename, cloudformation_data: {}, cfm_role_ end next end - MU.log "Merging policy #{name} into #{rolename}", MU::NOTICE, details: doc + MU.log "Merging policy #{name} into #{rolename}", details: JSON.pretty_generate(JSON.parse(doc)) MU::Cloud::AWS.iam.put_role_policy( - role_name: rolename, - policy_name: name, - policy_document: doc + role_name: rolename, + policy_name: name, + policy_document: doc ) } if cloudformation_data.size > 0 @@ -347,19 +350,16 @@ def self.createIAMProfile(rolename, base_profile: nil, extra_policies: nil, clou cfm_prof_name, prof_cfm_template = MU::Cloud::CloudFormation.cloudFormationBase("iamprofile", name: rolename) cloudformation_data.merge!(role_cfm_template) cloudformation_data.merge!(prof_cfm_template) - else - MU.log "Creating IAM role and policies for '#{name}' nodes" end if base_profile - MU.log "Incorporating policies from existing IAM profile '#{base_profile}'" resp = MU::Cloud::AWS.iam.get_instance_profile(instance_profile_name: base_profile) resp.instance_profile.roles.each { |baserole| role_policies = MU::Cloud::AWS.iam.list_role_policies(role_name: baserole.role_name).policy_names role_policies.each { |name| resp = MU::Cloud::AWS.iam.get_role_policy( - role_name: baserole.role_name, - policy_name: name + role_name: baserole.role_name, + policy_name: name ) policies[name] = URI.unescape(resp.policy_document) } @@ -378,10 +378,15 @@ def self.createIAMProfile(rolename, base_profile: nil, extra_policies: nil, clou } end if !cloudformation_data.nil? and cloudformation_data.size == 0 - resp = MU::Cloud::AWS.iam.create_role( - role_name: rolename, - assume_role_policy_document: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]}' - ) + begin + resp = MU::Cloud::AWS.iam.create_role( + role_name: rolename, + assume_role_policy_document: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]}' + ) + MU.log "Creating IAM role and policies for '#{rolename}' nodes" + rescue Aws::IAM::Errors::EntityAlreadyExists => e + MU.log "IAM role #{rolename} already exists, updating" + end end begin name=doc=nil @@ -390,11 +395,11 @@ def self.createIAMProfile(rolename, base_profile: nil, extra_policies: nil, clou MU::Cloud::CloudFormation.setCloudFormationProp(cloudformation_data[cfm_role_name], "Policies", { "PolicyName" => name, "PolicyDocument" => JSON.parse(doc) }) next end - MU.log "Merging policy #{name} into #{rolename}", MU::NOTICE, details: doc + MU.log "Merging policy #{name} into #{rolename}", details: JSON.pretty_generate(JSON.parse(doc)) MU::Cloud::AWS.iam.put_role_policy( - role_name: rolename, - policy_name: name, - policy_document: doc + role_name: rolename, + policy_name: name, + policy_document: doc ) } rescue Aws::IAM::Errors::MalformedPolicyDocument => e @@ -407,14 +412,24 @@ def self.createIAMProfile(rolename, base_profile: nil, extra_policies: nil, clou return [rolename, cfm_role_name, cfm_prof_name] end - resp = MU::Cloud::AWS.iam.create_instance_profile( + begin + resp = MU::Cloud::AWS.iam.create_instance_profile( instance_profile_name: rolename - ) + ) + rescue Aws::IAM::Errors::EntityAlreadyExists => e + resp = MU::Cloud::AWS.iam.get_instance_profile( + instance_profile_name: rolename + ) + end - MU::Cloud::AWS.iam.add_role_to_instance_profile( + begin + MU::Cloud::AWS.iam.add_role_to_instance_profile( instance_profile_name: rolename, role_name: rolename - ) + ) + rescue Aws::IAM::Errors::LimitExceeded => e + # also ok + end begin MU::Cloud::AWS.iam.get_instance_profile(instance_profile_name: rolename) @@ -871,7 +886,7 @@ def postBoot(instance_id = nil) cloud_id: instance.subnet_id ) if subnet.nil? - raise MuError, "Got null subnet id out of #{@config['vpc']}/#{instance.subnet_id}" + raise MuError, "Got null subnet id out of #{@config['vpc']} when asking for #{instance.subnet_id}" end end @@ -1002,16 +1017,31 @@ def postBoot(instance_id = nil) notify end - windows? ? ssh_wait = 60 : ssh_wait = 30 - windows? ? max_retries = 50 : max_retries = 35 begin - session = getSSHSession(max_retries, ssh_wait) - initialSSHTasks(session) + if windows? + # kick off certificate generation early; WinRM will need it + cert, key = @deploy.nodeSSLCerts(self) + if !@groomer.haveBootstrapped? + session = getWinRMSession(50, 60, reboot_on_problems: true) + initialWinRMTasks(session) + session.close + else # for an existing Windows node: WinRM, then SSH if it fails + begin + session = getWinRMSession(1, 60) + rescue Exception # yeah, yeah + session = getSSHSession(1, 60) + # XXX maybe loop at least once if this also fails? + end + end + else + session = getSSHSession(40, 30) + initialSSHTasks(session) + end rescue BootstrapTempFail - sleep ssh_wait + sleep 45 retry ensure - session.close if !session.nil? + session.close if !session.nil? and !windows? end if @config["existing_deploys"] && !@config["existing_deploys"].empty? @@ -1312,9 +1342,11 @@ def groom @groomer.saveDeployData begin - @groomer.run(purpose: "Full Initial Run", max_retries: 15) - rescue MU::Groomer::RunError - MU.log "Proceeding after failed initial Groomer run, but #{node} may not behave as expected!", MU::WARN + @groomer.run(purpose: "Full Initial Run", max_retries: 15, reboot_first_fail: windows?) + rescue MU::Groomer::RunError => e + MU.log "Proceeding after failed initial Groomer run, but #{node} may not behave as expected!", MU::WARN, details: e.message + rescue Exception => e + MU.log "Caught #{e.inspect} on #{node} in an unexpected place (after @groomer.run on Full Initial Run)", MU::ERR end if !@config['create_image'].nil? and !@config['image_created'] diff --git a/modules/mu/clouds/aws/server_pool.rb b/modules/mu/clouds/aws/server_pool.rb index b3aff03c1..c7444a659 100644 --- a/modules/mu/clouds/aws/server_pool.rb +++ b/modules/mu/clouds/aws/server_pool.rb @@ -436,7 +436,9 @@ def create kitten = MU::Cloud::Server.new(mommacat: @deploy, kitten_cfg: @config, cloud_id: member.instance_id) MU::MommaCat.lock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies") MU::MommaCat.unlock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies") - kitten.postBoot(member.instance_id) + if !kitten.postBoot(member.instance_id) + raise MU::Groomer::RunError, "Failure grooming #{member.instance_id}" + end kitten.groom MU::MommaCat.unlockAll } diff --git a/modules/mu/clouds/aws/vpc.rb b/modules/mu/clouds/aws/vpc.rb index e43e1e152..ae1d0e454 100644 --- a/modules/mu/clouds/aws/vpc.rb +++ b/modules/mu/clouds/aws/vpc.rb @@ -671,7 +671,7 @@ def groom end } else - MU.log "VPC #{peer_id} is not managed by this Mu server or is not configured to auto-accept peering requests. You must accept the peering request for '#{@config['name']}' (#{@config['vpc_id']}) by hand.", MU::NOTICE + MU.log "VPC #{peer_id} is not managed by this Mu server or is not configured to auto-accept peering requests. You must accept the peering request for '#{@config['name']}' (#{@config['vpc_id']}) by hand.", MU::WARN, details: "In the AWS Console, go to VPC => Peering Connections and look in the Actions drop-down. You can also set 'Invade Foreign VPCs' to 'true' using mu-configure to auto-accept all peering connections within this account, regardless of whether this Mu server owns the VPCs. This setting is per-user." end end diff --git a/modules/mu/config.rb b/modules/mu/config.rb index 9c9c56d5e..34e4ddb7a 100644 --- a/modules/mu/config.rb +++ b/modules/mu/config.rb @@ -331,7 +331,7 @@ def cloudCode(code, placeholder = "CLOUDCODEPLACEHOLDER") # templates. They're globals on purpose. Stop whining. $file_format = MU::Config.guessFormat(path) $yaml_refs = {} - erb = ERB.new(File.read(path)) + erb = ERB.new(File.read(path), nil, "<>") raw_text = erb.result(get_binding) raw_json = nil @@ -690,7 +690,7 @@ def self.include(file, binding = nil, param_pass = false) assume_type = :yaml end begin - erb = ERB.new(File.read(file)) + erb = ERB.new(File.read(file), nil, "<>") rescue Errno::ENOENT => e retries = retries + 1 if retries == 1 diff --git a/modules/mu/defaults/amazon_images.yaml b/modules/mu/defaults/amazon_images.yaml index d769bf584..69cb2cc31 100644 --- a/modules/mu/defaults/amazon_images.yaml +++ b/modules/mu/defaults/amazon_images.yaml @@ -67,19 +67,19 @@ ubuntu14: &ubuntu14 ap-southeast-1: ami-2855964b ap-southeast-2: ami-d19fc4b2 win2k12r2: &win2k12r2 - us-east-1: ami-d5651cc3 - us-east-2: ami-51b39434 - us-west-1: ami-80e9c8e0 - us-west-2: ami-942147f4 - eu-central-1: ami-a14f96ce - eu-west-1: ami-e9171c8f - sa-east-1: ami-bad8b7d6 - ap-northeast-1: ami-8edfe1e9 - ap-northeast-2: ami-0b21fc65 - ap-southeast-1: ami-adec6bce - ap-southeast-2: ami-f0061393 - ap-south-1: ami-69aad706 - ca-central-1: ami-906ed2f4 + us-east-1: ami-d4409aae + us-east-2: ami-fbbe929e + us-west-1: ami-ec91ac8c + us-west-2: ami-106ca068 + eu-central-1: ami-59e15a36 + eu-west-1: ami-65b16b1c + sa-east-1: ami-93d6afff + ap-northeast-1: ami-dcd375ba + ap-northeast-2: ami-fa2e8b94 + ap-southeast-1: ami-b61657d5 + ap-southeast-2: ami-9a7b97f8 + ap-south-1: ami-99a8eaf6 + ca-central-1: ami-608b3304 win2k16: &win2k16 us-east-1: ami-d2cb25a8 us-east-2: ami-2db59748 diff --git a/modules/mu/deploy.rb b/modules/mu/deploy.rb index 4f22c0892..307fa0141 100644 --- a/modules/mu/deploy.rb +++ b/modules/mu/deploy.rb @@ -202,7 +202,8 @@ def run "environment" => @environment, "seed" => MU.seed, "deployment_start_time" => @timestart, - "chef_user" => MU.chef_user + "chef_user" => MU.chef_user, + "mu_user" => MU.mu_user } @mommacat = MU::MommaCat.new( MU.deploy_id, diff --git a/modules/mu/groomer.rb b/modules/mu/groomer.rb index 7013ba3f7..4b7ef9fd9 100644 --- a/modules/mu/groomer.rb +++ b/modules/mu/groomer.rb @@ -110,6 +110,7 @@ def cleanup retval = @groomer_obj.method(method).call end rescue Exception => e + pp e.backtrace raise MU::Groomer::RunError, e.message, e.backtrace end @groom_semaphore.synchronize { diff --git a/modules/mu/groomers/chef.rb b/modules/mu/groomers/chef.rb index 36890ba61..e5240360b 100644 --- a/modules/mu/groomers/chef.rb +++ b/modules/mu/groomers/chef.rb @@ -55,7 +55,6 @@ def self.loadChefLib(user = MU.chef_user, env = "dev", mu_user = MU.mu_user) require 'chef/knife' require 'chef/knife/ssh' require 'chef/knife/bootstrap' - require 'chef/knife/bootstrap_windows_ssh' require 'chef/knife/node_delete' require 'chef/knife/client_delete' require 'chef/knife/data_bag_delete' @@ -64,6 +63,18 @@ def self.loadChefLib(user = MU.chef_user, env = "dev", mu_user = MU.mu_user) require 'chef/file_access_control/unix' require 'chef-vault' require 'chef-vault/item' + # XXX kludge to get at knife-windows when it's installed from + # a git repo and bundler sticks it somewhere in a corner + $LOAD_PATH.each { |path| + if path.match(/\/gems\/aws\-sdk\-core\-\d+\.\d+\.\d+\/lib$/) + addpath = path.sub(/\/gems\/aws\-sdk\-core\-\d+\.\d+\.\d+\/lib$/, "")+"/bundler/gems" + Dir.glob(addpath+"/knife-windows-*").each { |version| + $LOAD_PATH << version+"/lib" + } + end + } + require 'chef/knife/bootstrap_windows_winrm' + require 'chef/knife/bootstrap_windows_ssh' ::Chef::Config[:chef_server_url] = "https://#{MU.mu_public_addr}:7443/organizations/#{user}" if File.exists?("#{Etc.getpwnam(mu_user).dir}/.chef/knife.rb") MU.log "Loading Chef configuration from #{Etc.getpwnam(mu_user).dir}/.chef/knife.rb", MU::DEBUG @@ -238,9 +249,11 @@ def deleteSecret(vault: nil) # Invoke the Chef client on the node at the other end of a provided SSH # session. - # @param purpose [String] = A string describing the purpose of this client run. - # @param max_retries [Integer] = The maximum number of attempts at a successful run to make before giving up. - def run(purpose: "Chef run", update_runlist: true, max_retries: 5) + # @param purpose [String]: A string describing the purpose of this client run. + # @param max_retries [Integer]: The maximum number of attempts at a successful run to make before giving up. + # @param output [Boolean]: Display Chef's regular (non-error) output to the console + # @param override_runlist [String]: Use the specified run list instead of the node's configured list + def run(purpose: "Chef run", update_runlist: true, max_retries: 5, output: true, override_runlist: nil, reboot_first_fail: false) self.class.loadChefLib if update_runlist and !@config['run_list'].nil? knifeAddToRunList(multiple: @config['run_list']) @@ -254,34 +267,68 @@ def run(purpose: "Chef run", update_runlist: true, max_retries: 5) end saveDeployData - MU.log "Invoking Chef on #{@server.mu_name}: #{purpose}" retries = 0 try_upgrade = false output = [] error_signal = "CHEF EXITED BADLY: "+(0...25).map { ('a'..'z').to_a[rand(26)] }.join runstart = nil + cmd = nil + ssh = nil + winrm = nil + windows_try_ssh = false begin - ssh = @server.getSSHSession(max_retries) - cmd = nil - if !@server.windows? - if !@config["ssh_user"].nil? and !@config["ssh_user"].empty? and @config["ssh_user"] != "root" + runstart = Time.new + if !@server.windows? or windows_try_ssh + MU.log "Invoking Chef over ssh on #{@server.mu_name}: #{purpose}" + windows_try_ssh = false + ssh = @server.getSSHSession(max_retries) + if @server.windows? + cmd = "chef-client.bat --color || echo #{error_signal}" + elsif !@config["ssh_user"].nil? and !@config["ssh_user"].empty? and @config["ssh_user"] != "root" upgrade_cmd = try_upgrade ? "sudo curl -L https://chef.io/chef/install.sh | sudo version=#{MU.chefVersion} sh &&" : "" cmd = "#{upgrade_cmd} sudo chef-client --color || echo #{error_signal}" else upgrade_cmd = try_upgrade ? "curl -L https://chef.io/chef/install.sh | version=#{MU.chefVersion} sh &&" : "" cmd = "#{upgrade_cmd} chef-client --color || echo #{error_signal}" end + retval = ssh.exec!(cmd) { |ch, stream, data| + puts data + output << data + raise MU::Groomer::RunError, output.grep(/ ERROR: /).last if data.match(/#{error_signal}/) + } else - upgrade_cmd = try_upgrade ? "powershell \". { Invoke-WebRequest -useb https://omnitruck.chef.io/install.ps1 } | Invoke-Expression; Install-Project -version:#{MU.chefVersion} -download_directory:$HOME \" &&" : "" - cmd = "#{upgrade_cmd} $HOME/chef-client --color || echo #{error_signal}" + MU.log "Invoking Chef over WinRM on #{@server.mu_name}: #{purpose}" + winrm = @server.getWinRMSession(haveBootstrapped? ? 1 : max_retries) +MU.log "wtfsauce", MU::WARN + if @server.windows? and @server.windowsRebootPending?(winrm) + if retries > 3 + @server.reboot # sometimes it needs help + end + raise MU::Groomer::RunError, "#{@server.mu_name} has a pending reboot" + end + if try_upgrade + pp winrm.run("Invoke-WebRequest -useb https://omnitruck.chef.io/install.ps1 | Invoke-Expression; Install-Project -version:#{MU.chefVersion} -download_directory:$HOME") + end + output = [] + cmd = "c:/opscode/chef/bin/chef-client.bat --color" + if override_runlist + cmd = cmd + " -o '#{override_runlist}'" + end + resp = winrm.run(cmd) do |stdout, stderr| + if stdout + print stdout if output + output << stdout + end + if stderr + MU.log stderr, MU::ERR + output << stderr + end + end + if resp.exitcode != 0 + raise MU::Groomer::RunError, output.slice(output.length-50, output.length).join("") + end end - runstart = Time.new - retval = ssh.exec!(cmd) { |ch, stream, data| - puts data - output << data - raise MU::Groomer::RunError, output.grep(/ ERROR: /).last if data.match(/#{error_signal}/) - } - rescue RuntimeError, SystemCallError, Timeout::Error, SocketError, Errno::ECONNRESET, IOError, Net::SSH::Exception, MU::Groomer::RunError => e + rescue RuntimeError, SystemCallError, Timeout::Error, SocketError, Errno::ECONNRESET, IOError, Net::SSH::Exception, MU::Groomer::RunError, WinRM::WinRMError, MU::MuError => e begin ssh.close if !ssh.nil? rescue Net::SSH::Exception, IOError => e @@ -292,22 +339,43 @@ def run(purpose: "Chef run", update_runlist: true, max_retries: 5) end sleep 10 end - - if e.instance_of?(MU::Groomer::RunError) and retries == 0 + if e.instance_of?(MU::Groomer::RunError) and retries == 0 and max_retries > 1 MU.log "Got a run error, will attempt to install/update Chef Client on next attempt", MU::NOTICE try_upgrade = true else try_upgrade = false end + if e.is_a?(MU::Groomer::RunError) + if reboot_first_fail + try_upgrade = true + begin + preClean(true) # drop any Chef install that's not ours + @server.reboot # try gently rebooting the thing + rescue Exception => e # it's ok to fail here (and to ignore failure) + MU.log "preclean err #{e.inspect}", MU::ERR + end + reboot_first_fail = false + end + end + + # Effectively alternate between WinRM and ssh on Windows. Something + # will probably work eventually. Right? + if @server.windows? and haveBootstrapped? + windows_try_ssh = true + end + if retries < max_retries retries += 1 - MU.log "#{@server.mu_name}: Chef run '#{purpose}' failed after #{Time.new - runstart} seconds, retrying (#{retries}/#{max_retries})", MU::WARN, details: e.inspect + MU.log "#{@server.mu_name}: Chef run '#{purpose}' failed after #{Time.new - runstart} seconds, retrying (#{retries}/#{max_retries})", MU::WARN, details: e.message sleep 30 retry else raise MU::Groomer::RunError, "#{@server.mu_name}: Chef run '#{purpose}' failed #{max_retries} times, last error was: #{e.message}" end + rescue Exception => e + raise MU::Groomer::RunError, "Caught unexpected #{e.inspect} on #{@server.mu_name} in @groomer.run" + end saveDeployData @@ -345,34 +413,60 @@ def preClean(leave_ours = false) remove_cmd = "sudo rpm -e erase chef ; sudo rm -rf /var/chef/ /etc/chef /opt/chef/ /usr/bin/chef-* ; sudo apt-get -y remove chef ; sudo touch /opt/mu_installed_chef" end guardfile = "/opt/mu_installed_chef" - else - remove_cmd = "( rm -rf /cygdrive/c/chef/*.pem ; /cygdrive/c/Windows/system32/reg query HKLM\\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ /f 'Chef Client' /s /t REG_SZ | grep '}$' | cut -d{ -f2 | cut -d} -f1 | xargs msiexec /qn /x ) ; /cygdrive/c/Windows/system32/reg query HKLM\\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ /f 'Chef Client' /s /t REG_SZ | grep '}$' | cut -d{ -f2 | cut -d} -f1 | xargs msiexec /qn /x )" - guardfile = "/cygdrive/c/mu_installed_chef" - end - - ssh = @server.getSSHSession(15) - if leave_ours - MU.log "Expunging pre-existing Chef install on #{@server.mu_name}, if we didn't create it", MU::NOTICE - begin - ssh.exec!(%Q{test -f #{guardfile} || (#{remove_cmd}) ; touch #{guardfile}}) - rescue IOError => e - # TO DO - retry this in a cleaner way - MU.log "Got #{e.inspect} while trying to clean up chef, retrying", MU::NOTICE - ssh = @server.getSSHSession(15) - ssh.exec!(%Q{test -f #{guardfile} || (#{remove_cmd}) ; touch #{guardfile}}) + + ssh = @server.getSSHSession(15) + if leave_ours + MU.log "Expunging pre-existing Chef install on #{@server.mu_name}, if we didn't create it", MU::NOTICE + begin + ssh.exec!(%Q{test -f #{guardfile} || (#{remove_cmd}) ; touch #{guardfile}}) + rescue IOError => e + # TO DO - retry this in a cleaner way + MU.log "Got #{e.inspect} while trying to clean up chef, retrying", MU::NOTICE + ssh = @server.getSSHSession(15) + ssh.exec!(%Q{test -f #{guardfile} || (#{remove_cmd}) ; touch #{guardfile}}) + end + else + MU.log "Expunging pre-existing Chef install on #{@server.mu_name}", MU::NOTICE + ssh.exec!(remove_cmd) end + + ssh.close else - MU.log "Expunging pre-existing Chef install on #{@server.mu_name}", MU::NOTICE - ssh.exec!(remove_cmd) - end + remove_cmd = %Q{ + $uninstall_string = (Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* | Where-Object {$_.DisplayName -like "chef client*"}).UninstallString + if($uninstall_string){ + $uninstall_string = ($uninstall_string -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X","").Trim() + $($uninstall_string -Replace '[\\s\\t]+', ' ').Split() | ForEach { + start-process "msiexec.exe" -arg "/X $_ /qn" -Wait + } + } + Remove-Item c:/chef/ -Force -Recurse -ErrorAction Continue + Remove-Item c:/opscode/ -Force -Recurse -ErrorAction Continue + Remove-Item C:/Users/ADMINI~1/AppData/Local/Temp/bootstrap*.bat -Force -Recurse -ErrorAction Continue + Remove-Item C:/Users/ADMINI~1/AppData/Local/Temp/chef-* -Force -Recurse -ErrorAction Continue + } + shell = @server.getWinRMSession(15) + removechef = true + if leave_ours + resp = shell.run("Test-Path c:/mu_installed_chef") + if resp.stdout.chomp == "True" + MU.log "Found existing Chef installation created by Mu, leaving it alone" + removechef = false + end + end - ssh.close +# remove_cmd = %Q{$my_chef = (Get-ItemProperty $location | Where-Object {$_.DisplayName -like "chef client*"}).DisplayName + if removechef + MU.log "Expunging pre-existing Chef install on #{@server.mu_name}", MU::NOTICE, details: remove_cmd +# pp shell.run(remove_cmd) + end + end end # Bootstrap our server with Chef def bootstrap self.class.loadChefLib - createGenericHostSSLCert + stashHostSSLCertSecret if !@config['cleaned_chef'] begin preClean(true) @@ -385,6 +479,7 @@ def bootstrap end nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_addr, ssh_user, ssh_key_name = @server.getSSHConfig + MU.log "Bootstrapping #{@server.mu_name} (#{canonical_addr}) with knife" run_list = ["recipe[mu-tools::newclient]"] @@ -401,70 +496,93 @@ def bootstrap vault_access = [] end - if !@server.windows? - kb = ::Chef::Knife::Bootstrap.new([canonical_addr]) - kb.config[:use_sudo] = true - kb.config[:distro] = 'chef-full' - else - kb = ::Chef::Knife::BootstrapWindowsSsh.new([canonical_addr]) - kb.config[:cygwin] = true - # kb.config[:distro] = 'windows-chef-client-msi' - #Trying to solve random createprocess errors - knife-windows always installs 32bit and architecture/bootstrap_architecture don't seem to work - kb.config[:msi_url] = "https://www.chef.io/chef/download?p=windows&pv=2012&m=x86_64&v=#{MU.chefVersion}" - # kb.config[:node_ssl_verify_mode] = 'none' - # kb.config[:node_verify_api_cert] = false - end - - # XXX this seems to break Knife Bootstrap for the moment - # if vault_access.size > 0 - # v = {} - # vault_access.each { |vault| - # v[vault['vault']] = [] if v[vault['vault']].nil? - # v[vault['vault']] << vault['item'] - # } - # kb.config[:bootstrap_vault_json] = JSON.generate(v) - # end - - kb.config[:json_attribs] = JSON.generate(json_attribs) if json_attribs.size > 1 - kb.config[:run_list] = run_list - kb.config[:ssh_user] = ssh_user - kb.config[:forward_agent] = ssh_user - kb.name_args = "#{canonical_addr}" - kb.config[:chef_node_name] = @server.mu_name - kb.config[:bootstrap_version] = MU.chefVersion - # XXX key off of MU verbosity level - kb.config[:log_level] = :debug - kb.config[:identity_file] = "#{Etc.getpwuid(Process.uid).dir}/.ssh/#{ssh_key_name}" - # kb.config[:ssh_gateway] = "#{nat_ssh_user}@#{nat_ssh_host}" if !nat_ssh_host.nil? # Breaking bootsrap - # This defaults to localhost for some reason sometimes. Brute-force it. - - MU.log "Knife Bootstrap settings for #{@server.mu_name} (#{canonical_addr})", MU::NOTICE, details: kb.config - - retries = 0 @server.windows? ? max_retries = 25 : max_retries = 10 @server.windows? ? timeout = 720 : timeout = 300 + retries = 0 begin + if !@server.windows? + kb = ::Chef::Knife::Bootstrap.new([canonical_addr]) + kb.config[:use_sudo] = true + kb.name_args = "#{canonical_addr}" + kb.config[:distro] = 'chef-full' + kb.config[:ssh_user] = ssh_user + kb.config[:forward_agent] = ssh_user + kb.config[:identity_file] = "#{Etc.getpwuid(Process.uid).dir}/.ssh/#{ssh_key_name}" + else + kb = ::Chef::Knife::BootstrapWindowsWinrm.new([@server.mu_name]) + kb.name_args = [@server.mu_name] + kb.config[:manual] = true + kb.config[:winrm_transport] = :ssl + kb.config[:host] = @server.mu_name + kb.config[:winrm_port] = 5986 + kb.config[:session_timeout] = timeout + kb.config[:operation_timeout] = timeout + kb.config[:winrm_authentication_protocol] = :cert + kb.config[:winrm_client_cert] = "#{MU.mySSLDir}/#{@server.mu_name}-winrm.crt" + kb.config[:winrm_client_key] = "#{MU.mySSLDir}/#{@server.mu_name}-winrm.key" +# kb.config[:ca_trust_file] = "#{MU.mySSLDir}/Mu_CA.pem" + # XXX ca_trust_file doesn't work for some reason, so we have to set the below for now + kb.config[:winrm_ssl_verify_mode] = :verify_none + kb.config[:msi_url] = "https://www.chef.io/chef/download?p=windows&pv=2012&m=x86_64&v=#{MU.chefVersion}" + end + + # XXX this seems to break Knife Bootstrap + # if vault_access.size > 0 + # v = {} + # vault_access.each { |vault| + # v[vault['vault']] = [] if v[vault['vault']].nil? + # v[vault['vault']] << vault['item'] + # } + # kb.config[:bootstrap_vault_json] = JSON.generate(v) + # end + + kb.config[:json_attribs] = JSON.generate(json_attribs) if json_attribs.size > 1 + kb.config[:run_list] = run_list + kb.config[:chef_node_name] = @server.mu_name + kb.config[:bootstrap_version] = MU.chefVersion + # XXX key off of MU verbosity level + kb.config[:log_level] = :debug + # kb.config[:ssh_gateway] = "#{nat_ssh_user}@#{nat_ssh_host}" if !nat_ssh_host.nil? # Breaking bootsrap + + MU.log "Knife Bootstrap settings for #{@server.mu_name} (#{canonical_addr}), timeout set to #{timeout.to_s}", MU::NOTICE, details: kb.config + if @server.windows? and @server.windowsRebootPending? + raise MU::Cloud::BootstrapTempFail, "#{@server.mu_name} has a pending reboot" + end Timeout::timeout(timeout) { require 'chef' kb.run } # throws Net::HTTPServerException if we haven't really bootstrapped ::Chef::Node.load(@server.mu_name) - rescue Net::SSH::Disconnect, SystemCallError, Timeout::Error, Errno::ECONNRESET, Errno::EHOSTUNREACH, Net::SSH::Proxy::ConnectError, SocketError, Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, IOError, Net::HTTPServerException, SystemExit, Errno::ECONNREFUSED, Errno::EPIPE => e + rescue Net::SSH::Disconnect, SystemCallError, Timeout::Error, Errno::ECONNRESET, Errno::EHOSTUNREACH, Net::SSH::Proxy::ConnectError, SocketError, Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, IOError, Net::HTTPServerException, SystemExit, Errno::ECONNREFUSED, Errno::EPIPE, WinRM::WinRMError, HTTPClient::ConnectTimeoutError, RuntimeError, MU::Cloud::BootstrapTempFail => e if retries < max_retries retries += 1 - MU.log "#{@server.mu_name}: Knife Bootstrap failed #{e.inspect}, retrying (#{retries} of #{max_retries})", MU::WARN, details: e.backtrace - # bad Chef installs are possible culprits of bootstrap failures - if !@config['forced_preclean'] - preClean(false) + # Bad Chef installs are possible culprits of bootstrap failures, so + # try scrubbing them when that happens. + # On Windows, even a fresh install comes up screwy disturbingly + # often, so we let it start over from scratch if needed. Except for + # the first attempt, which usually fails due to WinRM funk. + if !e.is_a?(MU::Cloud::BootstrapTempFail) and + !(e.is_a?(WinRM::WinRMError) and @config['forced_preclean']) and + !@config['forced_preclean'] + begin + preClean(false) # it's ok for this to fail + rescue Exception => e + end MU::Groomer::Chef.cleanup(@server.mu_name, nodeonly: true) @config['forced_preclean'] = true + @server.reboot if @server.windows? # *sigh* end + MU.log "#{@server.mu_name}: Knife Bootstrap failed #{e.inspect}, retrying in #{(10*retries).to_s}s (#{retries} of #{max_retries})", MU::WARN, details: e.backtrace sleep 10*retries retry else raise MuError, "#{@server.mu_name}: Knife Bootstrap failed too many times with #{e.inspect}" end + rescue Exception => e +MU.log e.inspect, MU::ERR, details: e.backtrace +sleep 10*retries +retry end # Now that we're done, remove one-shot bootstrap recipes from the @@ -759,70 +877,18 @@ def knifeCmd(cmd, showoutput = false) self.class.knifeCmd(cmd, showoutput) end - def createGenericHostSSLCert - nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name = @server.getSSHConfig - # Manufacture a generic SSL certificate, signed by the Mu master, for - # consumption by various node services (Apache, Splunk, etc). - return if File.exists?("#{MU.mySSLDir}/#{@server.mu_name}.crt") - MU.log "Creating self-signed service SSL certificate for #{@server.mu_name} (CN=#{canonical_ip})" - - # Create and save a key - key = OpenSSL::PKey::RSA.new 4096 - if !Dir.exist?(MU.mySSLDir) - Dir.mkdir(MU.mySSLDir, 0700) - end - - open("#{MU.mySSLDir}/#{@server.mu_name}.key", 'w', 0600) { |io| - io.write key.to_pem - } - - # Create a certificate request for this node - csr = OpenSSL::X509::Request.new - csr.version = 0 - csr.subject = OpenSSL::X509::Name.parse "CN=#{canonical_ip}/O=Mu/C=US" - csr.public_key = key.public_key - open("#{MU.mySSLDir}/#{@server.mu_name}.csr", 'w', 0644) { |io| - io.write csr.to_pem - } - - - if MU.chef_user == "mu" - @server.deploy.signSSLCert("#{MU.mySSLDir}/#{@server.mu_name}.csr") - else - deploykey = OpenSSL::PKey::RSA.new(@server.deploy.public_key) - deploysecret = Base64.urlsafe_encode64(deploykey.public_encrypt(@server.deploy.deploy_secret)) - res_type = "server" - res_type = "server_pool" if !@config['basis'].nil? - uri = URI("https://#{MU.mu_public_addr}:2260/") - req = Net::HTTP::Post.new(uri) - req.set_form_data( - "mu_id" => MU.deploy_id, - "mu_resource_name" => @config['name'], - "mu_resource_type" => res_type, - "mu_ssl_sign" => "#{MU.mySSLDir}/#{@server.mu_name}.csr", - "mu_user" => MU.mu_user, - "mu_deploy_secret" => deploysecret - ) - http = Net::HTTP.new(uri.hostname, uri.port) - http.ca_file = "/etc/pki/Mu_CA.pem" # XXX why no worky? - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX this sucks - response = http.request(req) - - MU.log "Got error back on signing request for #{MU.mySSLDir}/#{@server.mu_name}.csr", MU::ERR if response.code != "200" - end + # Upload the certificate to a Chef Vault for this node + def stashHostSSLCertSecret + cert, key = @server.deploy.nodeSSLCerts(@server) - cert = OpenSSL::X509::Certificate.new File.read "#{MU.mySSLDir}/#{@server.mu_name}.crt" - # Upload the certificate to a Chef Vault for this node certdata = { - "data" => { - "node.crt" => cert.to_pem.chomp!.gsub(/\n/, "\\n"), - "node.key" => key.to_pem.chomp!.gsub(/\n/, "\\n") - } + "data" => { + "node.crt" => cert.to_pem.chomp!.gsub(/\n/, "\\n"), + "node.key" => key.to_pem.chomp!.gsub(/\n/, "\\n") + } } saveSecret(item: "ssl_cert", data: certdata, permissions: nil) - # Any and all 'secrets' parameters should also be stuffed into our vault. saveSecret(item: "secrets", data: @config['secrets'], permissions: nil) if !@config['secrets'].nil? end diff --git a/modules/mu/logger.rb b/modules/mu/logger.rb index ae1ebb270..0784a8ee5 100644 --- a/modules/mu/logger.rb +++ b/modules/mu/logger.rb @@ -79,12 +79,18 @@ def log(msg, Syslog.open("Mu/"+caller_name, Syslog::LOG_PID, Syslog::LOG_DAEMON | Syslog::LOG_LOCAL3) if !Syslog.opened? if !details.nil? - details = details[:details] if details.has_key?(:details) - details = PP.pp(details, '') + if details.is_a?(Hash) and details.has_key?(:details) + details = details[:details] + end + details = PP.pp(details, '') if !details.is_a?(String) end details = "
"+details+"
" if @html - # We get passed literal quoted newlines sometimes, fix 'em - details.gsub!(/\\n/, "\n") if !details.nil? + # We get passed literal quoted newlines sometimes, fix 'em. Get Windows' + # ugly line feeds too. + if !details.nil? + details.gsub!(/\\n/, "\n") + details.gsub!(/(\\r|\r)/, "") + end msg = msg.first if msg.is_a?(Array) msg = "" if msg == nil diff --git a/modules/mu/master/ldap.rb b/modules/mu/master/ldap.rb index 4957bc238..a6d7fce97 100755 --- a/modules/mu/master/ldap.rb +++ b/modules/mu/master/ldap.rb @@ -484,8 +484,10 @@ def self.findGroups(search = [], exact: false, searchbase: $MU_CFG['ldap']['base # @param search [Array]: Strings to search for. # @param exact [Boolean]: Return only exact matches for whole fields. # @param searchbase [String]: The DN under which to search. + # @param extra_attrs [Array]: Other LDAP attributes to search + # @param matchgroups [Array]: An array of groups. If supplied, a user must be a member of one of these in order to match. # @return [Array] - def self.findUsers(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn'], extra_attrs: []) + def self.findUsers(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_dn'], extra_attrs: [], matchgroups: []) # We want to search groups, but can't search on memberOf with wildcards. # So search groups independently, build a list of full CNs, and use # those. @@ -555,6 +557,9 @@ def self.findUsers(search = [], exact: false, searchbase: $MU_CFG['ldap']['base_ rescue NoMethodError next end + if matchgroups and matchgroups.size > 0 + next if (acct[:memberOf] & matchgroups).size < 1 + end users[acct[@uid_attr].first] = {} users[acct[@uid_attr].first]['dn'] = acct.dn getattrs.each { |attr| diff --git a/modules/mu/mommacat.rb b/modules/mu/mommacat.rb index 4b51e0535..90493e62f 100644 --- a/modules/mu/mommacat.rb +++ b/modules/mu/mommacat.rb @@ -21,7 +21,6 @@ autoload :Chef, 'chef' gem "chef-vault" autoload :ChefVault, 'chef-vault' -gem "knife-windows" require 'timeout' module MU @@ -226,15 +225,9 @@ def initialize(deploy_id, MU.log "Creating deploy secret for #{MU.deploy_id}" @deploy_secret = Password.random(256) if !@original_config['scrub_mu_isms'] - begin - MU::Cloud::AWS.s3(MU.myRegion).put_object( - acl: "private", - bucket: MU.adminBucketName, - key: "#{@deploy_id}-secret", - body: "#{@deploy_secret}" - ) - rescue Aws::S3::Errors::PermanentRedirect => e - raise DeployInitializeError, "Got #{e.inspect} trying to write #{@deploy_id}-secret to #{MU.adminBucketName}" + # TODO there's a nicer way to do this than hardcoding strings + if @clouds["AWS"] and @clouds["AWS"] > 0 + MU::Cloud::AWS.writeDeploySecret(@deploy_id, @deploy_secret) end end if set_context_to_me @@ -1153,20 +1146,33 @@ def self.findStray(cloud, elsif kittens.size == 0 # If we don't have a MU::Cloud object, manufacture a dummy one. # Give it a fake name if we have to and have decided that's ok. - if (name.nil? or name.empty?) and !dummy_ok - MU.log "Found cloud provider data for #{cloud} #{type} #{kitten_cloud_id}, but without a name I can't manufacture a proper #{type} object to return", MU::DEBUG, details: caller - next - else - if !mu_name.nil? - name = mu_name - elsif !tag_value.nil? - name = tag_value + if (name.nil? or name.empty?) + if !dummy_ok + MU.log "Found cloud provider data for #{cloud} #{type} #{kitten_cloud_id}, but without a name I can't manufacture a proper #{type} object to return", MU::DEBUG, details: caller + next else - name = kitten_cloud_id + if !mu_name.nil? + name = mu_name + elsif !tag_value.nil? + name = tag_value + else + name = kitten_cloud_id + end end end cfg = {"name" => name, "cloud" => cloud, "region" => r} - if !calling_deploy.nil? + # If we can at least find the config from the deploy this will + # belong with, use that, even if it's an ungroomed resource. + if !calling_deploy.nil? and + !calling_deploy.original_config.nil? and + !calling_deploy.original_config[type+"s"].nil? + calling_deploy.original_config[type+"s"].each { |s| + if s["name"] == name + cfg = s.dup + break + end + } + matches << resourceclass.new(mommacat: calling_deploy, kitten_cfg: cfg, cloud_id: kitten_cloud_id) else matches << resourceclass.new(mu_name: name, kitten_cfg: cfg, cloud_id: kitten_cloud_id) @@ -1640,7 +1646,7 @@ def self.removeInstanceFromEtcHosts(node) # @param system_name [String]: The node's local system name # @return [void] def self.addInstanceToEtcHosts(public_ip, chef_name = nil, system_name = nil) - return if MU.mu_user != "mu" + return if !["mu", "root"].include?(MU.mu_user) # XXX cover ipv6 case if public_ip.nil? or !public_ip.match(/^\d+\.\d+\.\d+\.\d+$/) or (chef_name.nil? and system_name.nil?) @@ -1651,7 +1657,7 @@ def self.addInstanceToEtcHosts(public_ip, chef_name = nil, system_name = nil) end File.readlines("/etc/hosts").each { |line| if line.match(/^#{public_ip} /) or (chef_name != nil and line.match(/ #{chef_name}(\s|$)/)) or (system_name != nil and line.match(/ #{system_name}(\s|$)/)) - MU.log("Attempt to add duplicate /etc/hosts entry: #{public_ip} #{chef_name} #{system_name}", MU::WARN) + MU.log "Ignoring attempt to add duplicate /etc/hosts entry: #{public_ip} #{chef_name} #{system_name}", MU::DEBUG return end } @@ -1920,11 +1926,37 @@ def listNodes return nodes end + # For a given (Windows) server, return it's administrator user and password. + # This is generally for requests made to MommaCat from said server, which + # we can assume have been authenticated with the deploy secret. + # @param server [MU::Cloud::Server]: The Server object whose credentials we're fetching. + def retrieveWindowsAdminCreds(server) + if server.nil? + raise MuError, "retrieveWindowsAdminCreds must be called with a Server object" + elsif !server.is_a?(MU::Cloud::Server) + raise MuError, "retrieveWindowsAdminCreds must be called with a Server object (got #{server.class.name})" + end + if server.config['use_cloud_provider_windows_password'] + return [server.config["windows_admin_username"], server.getWindowsAdminPassword] + elsif server.config['windows_auth_vault'] && !server.config['windows_auth_vault'].empty? + if server.config["windows_auth_vault"].has_key?("password_field") + return [server.config["windows_admin_username"], + server.groomer.getSecret( + vault: server.config['windows_auth_vault']['vault'], + item: server.config['windows_auth_vault']['item'], + field: server.config["windows_auth_vault"]["password_field"] + )] + else + return [server.config["windows_admin_username"], server.getWindowsAdminPassword] + end + end + [] + end # Given a Certificate Signing Request, sign it with our internal CA and # writers the resulting signed certificate. Only works on local files. # @param csr_path [String]: The CSR to sign, as a file. - def signSSLCert(csr_path) + def signSSLCert(csr_path, sans = []) # XXX more sanity here, this feels unsafe certdir = File.dirname(csr_path) certname = File.basename(csr_path, ".csr") @@ -1934,12 +1966,17 @@ def signSSLCert(csr_path) end MU.log "Signing SSL certificate request #{csr_path} with #{MU.mySSLDir}/Mu_CA.pem" - csr = OpenSSL::X509::Request.new File.read csr_path + begin + csr = OpenSSL::X509::Request.new File.read csr_path + rescue Exception => e + MU.log e.message, MU::ERR, details: File.read(csr_path) + raise e + end + key = OpenSSL::PKey::RSA.new File.read "#{certdir}/#{certname}.key" # Load up the Mu Certificate Authority cakey = OpenSSL::PKey::RSA.new File.read "#{MU.mySSLDir}/Mu_CA.key" cacert = OpenSSL::X509::Certificate.new File.read "#{MU.mySSLDir}/Mu_CA.pem" - cur_serial = 0 File.open("#{MU.mySSLDir}/serial", File::CREAT|File::RDWR, 0600) { |f| f.flock(File::LOCK_EX) @@ -1955,13 +1992,25 @@ def signSSLCert(csr_path) # Create a certificate from our CSR, signed by the Mu CA cert = OpenSSL::X509::Certificate.new cert.serial = cur_serial - cert.version = 2 + cert.version = 3 cert.not_before = Time.now - cert.not_after = Time.now + 1800000 # 500 days + cert.not_after = Time.now + 180000000 cert.subject = csr.subject cert.public_key = csr.public_key cert.issuer = cacert.subject - cert.sign cakey, OpenSSL::Digest::SHA1.new + if !sans.nil? and sans.size > 0 + MU.log "Incorporting Subject Alternative Names: #{sans.join(",")}" + ef = OpenSSL::X509::ExtensionFactory.new + ef.issuer_certificate = cacert +#v3_req_client + ef.subject_certificate = cert + ef.subject_request = csr + cert.add_extension(ef.create_extension("keyUsage","nonRepudiation,digitalSignature,keyEncipherment", false)) + cert.add_extension(ef.create_extension("subjectAltName",sans.join(","),false)) +# XXX only do this if we see the otherName thinger in the san list + cert.add_extension(ef.create_extension("extendedKeyUsage","clientAuth,serverAuth,codeSigning,emailProtection",false)) + end + cert.sign cakey, OpenSSL::Digest::SHA256.new open("#{certdir}/#{certname}.crt", 'w', 0644) { |io| io.write cert.to_pem @@ -1970,6 +2019,7 @@ def signSSLCert(csr_path) owner_uid = Etc.getpwnam(MU.mu_user).uid File.chown(owner_uid, nil, "#{certdir}/#{certname}.crt") end + end # Make sure deployment data is synchronized to/from each node in the @@ -2059,6 +2109,125 @@ def syncLitter(nodeclasses = [], triggering_node: nil, save_all_only: false) MU.log "Synchronization of #{@deploy_id} complete", MU::DEBUG, details: update_servers end + # Given a MU::Cloud::Server object, return the generic self-signed SSL + # certficate we made for it. If one doesn't exist yet, generate it first. + # If it's a Windows node, also generate a certificate for WinRM client auth. + # @param server [MU::Cloud::Server]: The server for which to generate or return the cert + def nodeSSLCerts(server) + nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name = server.getSSHConfig + + certs = {} + results = {} + + if File.exists?("#{MU.mySSLDir}/#{server.mu_name}.crt") and + File.exists?("#{MU.mySSLDir}/#{server.mu_name}.key") + ext_cert = OpenSSL::X509::Certificate.new(File.read("#{MU.mySSLDir}/#{server.mu_name}.crt")) + if ext_cert.not_after < Time.now + MU.log "Node certificate for #{server.mu_name} is expired, regenerating", MU::WARN + ["crt", "key", "csr"].each { |suffix| + if File.exists?("#{MU.mySSLDir}/#{server.mu_name}.#{suffix}") + File.unlink("#{MU.mySSLDir}/#{server.mu_name}.#{suffix}") + end + } + else + results[server.mu_name] = [ + OpenSSL::X509::Certificate.new(File.read("#{MU.mySSLDir}/#{server.mu_name}.crt")), + OpenSSL::PKey::RSA.new(File.read("#{MU.mySSLDir}/#{server.mu_name}.key")) + ] + end + end + if results.size == 0 + certs[server.mu_name] = { + "sans" => ["IP:#{canonical_ip}"], + "cn" => server.mu_name + } + end + + if server.windows? + if File.exists?("#{MU.mySSLDir}/#{server.mu_name}-winrm.crt") and + File.exists?("#{MU.mySSLDir}/#{server.mu_name}-winrm.key") + results[server.mu_name+"-winrm"] = [File.read("#{MU.mySSLDir}/#{server.mu_name}-winrm.crt"), File.read("#{MU.mySSLDir}/#{server.mu_name}-winrm.key")] + else + certs[server.mu_name+"-winrm"] = { + "sans" => ["otherName:1.3.6.1.4.1.311.20.2.3;UTF8:#{server.config['windows_admin_username']}@localhost"], + "cn" => server.config['windows_admin_username'] + } + end + end + + certs.each { |certname, data| + MU.log "Generating SSL certificate #{certname} for #{server}" + + # Create and save a key + key = OpenSSL::PKey::RSA.new 4096 + if !Dir.exist?(MU.mySSLDir) + Dir.mkdir(MU.mySSLDir, 0700) + end + + open("#{MU.mySSLDir}/#{certname}.key", 'w', 0600) { |io| + io.write key.to_pem + } + # Create a certificate request for this node + csr = OpenSSL::X509::Request.new + csr.version = 3 + csr.subject = OpenSSL::X509::Name.parse "CN=#{data['cn']}/O=Mu/C=US" + csr.public_key = key.public_key + csr.sign key, OpenSSL::Digest::SHA256.new + open("#{MU.mySSLDir}/#{certname}.csr", 'w', 0644) { |io| + io.write csr.to_pem + } + if MU.chef_user == "mu" + signSSLCert("#{MU.mySSLDir}/#{certname}.csr", data['sans']) + else + deploykey = OpenSSL::PKey::RSA.new(public_key) + deploysecret = Base64.urlsafe_encode64(deploykey.public_encrypt(deploy_secret)) + res_type = "server" + res_type = "server_pool" if !server.config['basis'].nil? + uri = URI("https://#{MU.mu_public_addr}:2260/") + req = Net::HTTP::Post.new(uri) + req.set_form_data( + "mu_id" => MU.deploy_id, + "mu_resource_name" => server.config['name'], + "mu_resource_type" => res_type, + "mu_ssl_sign" => "#{MU.mySSLDir}/#{certname}.csr", + "mu_ssl_sans" => data["sans"].join(","), + "mu_user" => MU.mu_user, + "mu_deploy_secret" => deploysecret + ) + http = Net::HTTP.new(uri.hostname, uri.port) + http.ca_file = "/etc/pki/Mu_CA.pem" # XXX why no worky? + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX this sucks + response = http.request(req) + + MU.log "Got error back on signing request for #{MU.mySSLDir}/#{certname}.csr", MU::ERR if response.code != "200" + end + + cert = OpenSSL::X509::Certificate.new File.read "#{MU.mySSLDir}/#{certname}.crt" + results[certname] = [cert, key] + + pfx = nil + if server.windows? + cacert = OpenSSL::X509::Certificate.new File.read "#{MU.mySSLDir}/Mu_CA.pem" + pfx = OpenSSL::PKCS12.create(nil, nil, key, cert, [cacert], nil, nil, nil, nil) + open("#{MU.mySSLDir}/#{certname}.pfx", 'w', 0644) { |io| + io.write pfx.to_der + } + end + + if server.config['cloud'] == "AWS" + MU::Cloud::AWS.writeDeploySecret(@deploy_id, cert.to_pem, certname+".crt") + MU::Cloud::AWS.writeDeploySecret(@deploy_id, key.to_pem, certname+".key") + if server.windows? + MU::Cloud::AWS.writeDeploySecret(@deploy_id, pfx.to_der, certname+".pfx") + end +# XXX add google logic, or better yet abstract this method + end + } + + results[server.mu_name] + end + private # Check to see whether a given resource name is unique across all diff --git a/modules/mu/userdata/windows.erb b/modules/mu/userdata/windows.erb index 8e29f6066..d71a4de84 100644 --- a/modules/mu/userdata/windows.erb +++ b/modules/mu/userdata/windows.erb @@ -1,305 +1,272 @@ - Set-ExecutionPolicy Unrestricted -Force -Scope CurrentUser - - $sshdUser = "sshd_service" - $logfile = "c:/Mu-Bootstrap-$([Environment]::UserName).log" - $base_dir = 'c:/bin' - $cygwin_dir = "$base_dir/cygwin" - $username = (whoami).Split('\')[1] - $WebClient = New-Object System.Net.WebClient - $aws_meta = "http://169.254.169.254/latest" - - function log - { - $args - Add-Content "c:/Mu-Bootstrap-$([Environment]::UserName).log" "$(Get-Date -f MM-dd-yyyy_HH:mm:ss) $args" - Add-Content "c:/Mu-Bootstrap-GLOBAL.log" "$(Get-Date -f MM-dd-yyyy_HH:mm:ss) $args" +Set-ExecutionPolicy Unrestricted -Force -Scope CurrentUser + +$sshdUser = "sshd_service" +$tmp = "$env:Temp\mu-userdata" +mkdir $tmp +$logfile = "c:/Mu-Bootstrap-$([Environment]::UserName).log" +$basedir = 'c:/bin' +$cygwin_dir = "$basedir/cygwin" +$username = (whoami).Split('\')[1] +$WebClient = New-Object System.Net.WebClient +$awsmeta = "http://169.254.169.254/latest" +$pydir = 'c:\bin\python\python27' +$pyv = '2.7.14' +$env:Path += ";$pydir\Scripts;$pydir" + +function log +{ + Write-Host $args + Add-Content "c:/Mu-Bootstrap-$([Environment]::UserName).log" "$(Get-Date -f MM-dd-yyyy_HH:mm:ss) $args" + Add-Content "c:/Mu-Bootstrap-GLOBAL.log" "$(Get-Date -f MM-dd-yyyy_HH:mm:ss) $args" +} + +function fetchSecret([string]$file){ + log "Fetching s3://<%= MU.adminBucketName %>/$file to $tmp/$file" + aws.cmd s3 cp s3://<%= MU.adminBucketName %>/$file $tmp/$file +} + +function importCert([string]$cert, [string]$store){ + fetchSecret($cert) + if($cert -Match ".pfx$"){ + return Import-PfxCertificate -FilePath $tmp/$cert -CertStoreLocation Cert:\LocalMachine\$store + } else { + return Import-Certificate -FilePath $tmp/$cert -CertStoreLocation Cert:\LocalMachine\$store } +} - function Disable-SSHD - { - if ((Get-Service "sshd" -ErrorAction SilentlyContinue) -and (Test-Path "$cygwin_dir/bin/bash.exe")) { - log "Disabling pre-existing sshd" +log "- Invoked as $([Environment]::UserName) (system started at $(Get-CimInstance -ClassName win32_operatingsystem | select lastbootuptime)) -" - Stop-Service -ErrorAction SilentlyContinue sshd - Stop-Process -ProcessName sshd -force -ErrorAction SilentlyContinue - Invoke-Expression '& $cygwin_dir/bin/bash --login -c "cygrunsrv --stop sshd; cygrunsrv --remove sshd; net user sshd /delete; net user sshd_service /delete; mkpasswd > /etc/passwd"' - } - } - - log "----- Invoked as $([Environment]::UserName) (system started at $(Get-CimInstance -ClassName win32_operatingsystem | select lastbootuptime)) -----" - - <% if !$mu.skipApplyUpdates %> - If (!(Test-Path "c:/mu-installer-ran-updates")){ - Stop-Service -ErrorAction SilentlyContinue sshd - } - <% end %> +<% if !$mu.skipApplyUpdates %> +If (!(Test-Path "c:/mu-installer-ran-updates")){ + Stop-Service -ErrorAction SilentlyContinue sshd +} +<% end %> - <% if $mu.platform != "win2k16" %> - If ([Environment]::OSVersion.Version.Major -lt 10) { - If ("$($myInvocation.MyCommand.Path)" -ne "$env:Temp/realuserdata_stripped.ps1"){ +<% if $mu.platform != "win2k16" %> +If ([Environment]::OSVersion.Version.Major -lt 10) { + If ("$($myInvocation.MyCommand.Path)" -ne "$tmp/realuserdata_stripped.ps1"){ + $Error.Clear() + Invoke-WebRequest -Uri "$awsmeta/user-data" -OutFile $tmp/realuserdata.ps1 + while($Error.count -gt 0){ $Error.Clear() - Invoke-WebRequest -Uri "$aws_meta/user-data" -OutFile $env:Temp/realuserdata.ps1 - while($Error.count -gt 0){ - $Error.Clear() - log "Failed to retrieve current userdata from $aws_meta/user-data, waiting 15s and retrying" - sleep 15 - Invoke-WebRequest -Uri "$aws_meta/user-data" -OutFile $env:Temp/realuserdata.ps1 - } - Get-Content $env:Temp/realuserdata.ps1 | Select-String -pattern '^#','^<' -notmatch | Set-Content $env:Temp/realuserdata_stripped.ps1 - If (Compare-Object (Get-Content $myInvocation.MyCommand.Path) (Get-Content $env:Temp/realuserdata_stripped.ps1)){ - log "Invoking $env:Temp/realuserdata.ps1 in lieu of $($myInvocation.MyCommand.Path)" - Invoke-Expression $env:Temp/realuserdata_stripped.ps1 - exit - } + log "Failed to retrieve current userdata from $awsmeta/user-data, waiting 15s and retrying" + sleep 15 + Invoke-WebRequest -Uri "$awsmeta/user-data" -OutFile $tmp/realuserdata.ps1 + } + Get-Content $tmp/realuserdata.ps1 | Select-String -pattern '^#','^<' -notmatch | Set-Content $tmp/realuserdata_stripped.ps1 + If (Compare-Object (Get-Content $myInvocation.MyCommand.Path) (Get-Content $tmp/realuserdata_stripped.ps1)){ + log "Invoking $tmp/realuserdata.ps1 in lieu of $($myInvocation.MyCommand.Path)" + Invoke-Expression $tmp/realuserdata_stripped.ps1 + exit } } - <% end %> - $admin_username = (Get-WmiObject -Query 'Select * from Win32_UserAccount Where (LocalAccount=True and SID like "%-500")').name - log "Local admin account is $admin_username" - - Add-Type -Assembly System.Web - $password = [Web.Security.Membership]::GeneratePassword(15,2) - - If (!(Test-Path $base_dir)){ - New-Item -type directory -path $base_dir +} +<% end %> +$admin_username = (Get-WmiObject -Query 'Select * from Win32_UserAccount Where (LocalAccount=True and SID like "%-500")').name +log "Local admin: $admin_username" + +Add-Type -Assembly System.Web +$password = [Web.Security.Membership]::GeneratePassword(15,2) + +If (!(Test-Path $basedir)){ + mkdir $basedir +} + +<% if $mu.platform != "win2k16" %> +If ([Environment]::OSVersion.Version.Major -lt 10) { + If (!(Get-ScheduledTask -TaskName 'run-userdata')){ + log "Adding run-userdata scheduled task (user NT AUTHORITY\SYSTEM)" + Invoke-WebRequest -Uri "https://s3.amazonaws.com/cap-public/run-userdata_scheduledtask.xml" -OutFile $tmp/run-userdata_scheduledtask.xml + Register-ScheduledTask -Xml (Get-Content "$tmp/run-userdata_scheduledtask.xml" | out-string) -TaskName 'run-userdata' -Force -User "NT AUTHORITY\SYSTEM" } +} +<% end %> +$awsid=(New-Object System.Net.WebClient).DownloadString("$awsmeta/meta-data/instance-id") - <% if $mu.platform != "win2k16" %> - If ([Environment]::OSVersion.Version.Major -lt 10) { - If (!(Get-ScheduledTask -TaskName 'run-userdata')){ - log "Adding run-userdata scheduled task (user NT AUTHORITY\SYSTEM)" - Invoke-WebRequest -Uri "https://s3.amazonaws.com/cap-public/run-userdata_scheduledtask.xml" -OutFile $env:Temp/run-userdata_scheduledtask.xml - Register-ScheduledTask -Xml (Get-Content "$env:Temp/run-userdata_scheduledtask.xml" | out-string) -TaskName 'run-userdata' -Force -User "NT AUTHORITY\SYSTEM" - } +If (!(Test-Path $tmp/PSWindowsUpdate.zip)){ + If (!(Test-Path c:/Users/$admin_username/Documents/WindowsPowerShell/Modules)){ + mkdir c:/Users/$admin_username/Documents/WindowsPowerShell/Modules } - <% end %> - $instanceid=(New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") - If (!(Test-Path $env:Temp/PSWindowsUpdate.zip)){ - If (!(Test-Path c:/Users/$admin_username/Documents/WindowsPowerShell/Modules)){ - mkdir c:/Users/$admin_username/Documents/WindowsPowerShell/Modules - } - - $WebClient.DownloadFile("https://s3.amazonaws.com/cap-public/PSWindowsUpdate.zip","$env:Temp/PSWindowsUpdate.zip") - Add-Type -A 'System.IO.Compression.FileSystem' + $WebClient.DownloadFile("https://s3.amazonaws.com/cap-public/PSWindowsUpdate.zip","$tmp/PSWindowsUpdate.zip") + Add-Type -A 'System.IO.Compression.FileSystem' - If (!(Test-Path c:/windows/System32/WindowsPowerShell/v1.0/Modules/PSWindowsUpdate)){ - log "Extracting PSWindowsUpdate module to c:/windows/System32/WindowsPowerShell/v1.0/Modules" - [IO.Compression.ZipFile]::ExtractToDirectory("$env:Temp/PSWindowsUpdate.zip", "c:/windows/System32/WindowsPowerShell/v1.0/Modules") - } - If (!(Test-Path c:/Users/$admin_username/Documents/WindowsPowerShell/Modules/PSWindowsUpdate)){ - log "Extracting PSWindowsUpdate module to c:/Users/$admin_username/Documents/WindowsPowerShell" - [IO.Compression.ZipFile]::ExtractToDirectory("$env:Temp/PSWindowsUpdate.zip", "c:/Users/$admin_username/Documents/WindowsPowerShell/Modules") - } + If (!(Test-Path c:/windows/System32/WindowsPowerShell/v1.0/Modules/PSWindowsUpdate)){ + log "Extracting PSWindowsUpdate module to c:/windows/System32/WindowsPowerShell/v1.0/Modules" + [IO.Compression.ZipFile]::ExtractToDirectory("$tmp/PSWindowsUpdate.zip", "c:/windows/System32/WindowsPowerShell/v1.0/Modules") } + If (!(Test-Path c:/Users/$admin_username/Documents/WindowsPowerShell/Modules/PSWindowsUpdate)){ + log "Extracting PSWindowsUpdate module to c:/Users/$admin_username/Documents/WindowsPowerShell" + [IO.Compression.ZipFile]::ExtractToDirectory("$tmp/PSWindowsUpdate.zip", "c:/Users/$admin_username/Documents/WindowsPowerShell/Modules") + } +} - log "Setting Windows Update parameters in registry" - Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" -Name AUOptions -Value 3 - - If (!(Test-Path "$cygwin_dir/Cygwin.bat")){ - If (!(Test-Path $env:Temp/setup-x86_64.exe)){ - $WebClient.DownloadFile("http://cygwin.com/setup-x86_64.exe","$env:Temp/setup-x86_64.exe") - } - - If (!(Test-Path $env:Temp/cygwin.zip)){ - log "Downloading Cygwin packages" - $WebClient.DownloadFile("https://s3.amazonaws.com/mu-stuff/cygwin_20161022.zip","$env:Temp/cygwin.zip") - } - - Add-Type -A 'System.IO.Compression.FileSystem' - If (!(Test-Path $env:Temp/cygwin)){ - [IO.Compression.ZipFile]::ExtractToDirectory("$env:Temp/cygwin.zip", "$env:Temp/cygwin") - } - - log "Running Cygwin installer" - Start-Process -wait -FilePath "$env:Temp/setup-x86_64.exe" -ArgumentList "-q -n -l $env:Temp -l $env:Temp\cygwin -L -R $cygwin_dir -P openssh,mintty,vim,curl,openssl" - } - - if (!(Get-Service "sshd" -ErrorAction SilentlyContinue)){ - log "Invoking ssh-host-config to enable sshd as $sshdUser (I am $admin_username)" - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "ssh-host-config -y -c ntsec -w ''$password'' -u $sshdUser" > $cygwin_dir/sshd_setup_log.txt' - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "sed -i.bak ''s/#.*StrictModes.*yes/StrictModes no/'' /etc/sshd_config" >> $cygwin_dir/sshd_setup_log.txt' - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "sed -i.bak ''s/#.*PasswordAuthentication.*yes/PasswordAuthentication no/'' /etc/sshd_config" >> $cygwin_dir/sshd_setup_log.txt' - New-Item $cygwin_dir/sshd_installed_by.txt -type file -force -value $admin_username - log "Creating c:/$instanceid (<%= $mu.muID %>)" - New-Item c:/$instanceid -type file -force -value "<%= $mu.muID %>" - log "Value in that file: $(Get-Content c:/$instanceid)" - } - - log "Ensuring domain or local users are in /etc/passwd for sshd" - if((Get-WmiObject win32_computersystem).partofdomain){ - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkpasswd -d > /etc/passwd"' - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkgroup -l -d > /etc/group"' - } else { - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkpasswd -l > /etc/passwd"' - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "mkgroup -l > /etc/group"' - } - - if (!(Get-WmiObject win32_computersystem).partofdomain){ - If (!(Test-Path "c:/mu-configure-initial-ssh-user")){ - log "making sure the ssh user is configured correctly" - (([adsi]("WinNT://./$sshdUser, user")).psbase.invoke('SetPassword', "$password")) - $sshd_service = Get-WmiObject Win32_Service -Filter "Name='sshd'" - $sshd_service.Change($Null,$Null,$Null,$Null,$Null,$Null,".\$sshdUser",$password,$Null,$Null,$Null) - - $editrights="$cygwin_dir/bin/editrights" - &$editrights -a SeAssignPrimaryTokenPrivilege -u $sshdUser - &$editrights -a SeCreateTokenPrivilege -u $sshdUser - &$editrights -a SeTcbPrivilege -u $sshdUser - &$editrights -a SeServiceLogonRight -u $sshdUser - Add-Content c:/mu-configure-initial-ssh-user "done" - } - } - - $sshd_svc_user = (Get-WmiObject -Query "SELECT * FROM win32_service WHERE name='sshd'").StartName - if ( $sshd_svc_user.contains("\") ){ - $sshd_svc_user = $sshd_svc_user.substring($sshd_svc_user.LastIndexOf("\")+1) - } - log "Chowning /var/empty, /var/log/sshd.log, and /etc/ssh* to $sshd_svc_user" - Invoke-Expression -Debug '& $cygwin_dir/bin/bash --login -c "chown $sshd_svc_user /var/empty /var/log/sshd.log /etc/ssh*; chmod 755 /var/empty"' - - If (!((Get-ItemProperty HKLM:/SYSTEM/CurrentControlSet/Control/Lsa)."Authentication Packages" | Select-String -pattern "cyglsa64.dll")){ - If ((Test-Path "$cygwin_dir/bin/cyglsa-config")){ - log "Setting Cygwin LSA support, will reboot" - Invoke-Expression '& $cygwin_dir/bin/bash --login -c "echo yes | /bin/cyglsa-config"' - $need_reboot = $TRUE - } else { - log "Need to set Cygwin LSA support, but I don't see $cygwin_dir/bin/cyglsa-config!" - } - } - - $python_path = 'c:\bin\python\python27' - $env:Path += ";$python_path\Scripts;$python_path" - If (!(Test-Path "$python_path\python.exe")){ - If (!(Test-Path $env:Temp/python-2.7.9.msi)){ - log "Downloading Python installer" - $WebClient.DownloadFile("https://www.python.org/ftp/python/2.7.9/python-2.7.9.msi","$env:Temp/python-2.7.9.msi") - } - log "Running Python installer" - (Start-Process -FilePath msiexec -ArgumentList "/i $env:Temp\python-2.7.9.msi /qn ALLUSERS=1 TARGETDIR=$python_path" -Wait -Passthru).ExitCode - } - - If (!(Test-Path "$python_path\Scripts\aws.cmd")){ - If (!(Test-Path $env:Temp/get-pip.py)){ - log "Downloading get-pip.py" - $WebClient.DownloadFile("https://bootstrap.pypa.io/get-pip.py","$env:Temp/get-pip.py") - } - python $env:Temp/get-pip.py - log "Running pip install awscli" - pip install awscli - } +log "Setting Windows Update parameters in registry" +Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" -Name AUOptions -Value 3 - function removeChef($location){ +If (!(Test-Path "$pydir\python.exe")){ + If (!(Test-Path $tmp\python-$pyv.msi)){ + log "Downloading Python installer" + $WebClient.DownloadFile("https://www.python.org/ftp/python/$pyv/python-$pyv.msi","$tmp/python-$pyv.msi") + } + log "Running Python installer" + (Start-Process -FilePath msiexec -ArgumentList "/i $tmp\python-$pyv.msi /qn ALLUSERS=1 TARGETDIR=$pydir" -Wait -Passthru).ExitCode +} + +If (!(Test-Path "$pydir\Scripts\aws.cmd")){ + If (!(Test-Path $tmp/get-pip.py)){ + log "Downloading get-pip.py" + $WebClient.DownloadFile("https://bootstrap.pypa.io/get-pip.py","$tmp/get-pip.py") + } + python $tmp/get-pip.py + log "Running pip install awscli" + pip install awscli +} + +function removeChef($location){ + $install_chef = $false + $my_chef = (Get-ItemProperty $location | Where-Object {$_.DisplayName -like "chef client*"}).DisplayName + if ($my_chef) { + if ($my_chef -match '<%= MU.chefVersion %>'.split('-')[0]) { $install_chef = $false - $my_chef = (Get-ItemProperty $location | Where-Object {$_.DisplayName -like "chef client*"}).DisplayName - if ($my_chef) { - if ($my_chef -match '<%= MU.chefVersion %>'.split('-')[0]) { - $install_chef = $false - } else{ - log "Uninstalling Chef" - $uninstall_string = (Get-ItemProperty $location | Where-Object {$_.DisplayName -like "chef client*"}).UninstallString - $uninstall_string = ($uninstall_string -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X","").Trim() - $($uninstall_string -Replace '[\s\t]+', ' ').Split() | ForEach { - log "msiexec.exe /X $_ /gn" - start-process "msiexec.exe" -arg "/X $_ /qn" -Wait - } - $install_chef = $true - } + } else{ + log "Uninstalling Chef" + $uninstall_string = (Get-ItemProperty $location | Where-Object {$_.DisplayName -like "chef client*"}).UninstallString + $uninstall_string = ($uninstall_string -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X","").Trim() + $($uninstall_string -Replace '[\s\t]+', ' ').Split() | ForEach { + log "msiexec.exe /X $_ /gn" + start-process "msiexec.exe" -arg "/X $_ /qn" -Wait } - - return $install_chef - } - - If (!(Test-Path "c:\opscode\chef\embedded\bin\ruby.exe")){ $install_chef = $true - } else { - if (removeChef("HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*")){ - $install_chef = $true - } elseif (removeChef("HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*")) { - $install_chef = $true - } else { - $install_chef = $false - } - } - - If ($install_chef){ - log "Installing Chef" - If (!(Test-Path $env:Temp/chef-installer-<%= MU.chefVersion %>.msi)){ - log "Downloading Chef installer" - $WebClient.DownloadFile("https://www.chef.io/chef/download?p=windows&pv=2012&m=x86_64&v=<%= MU.chefVersion %>","$env:Temp/chef-installer-<%= MU.chefVersion %>.msi") - } - log "Running Chef installer" - (Start-Process -FilePath msiexec -ArgumentList "/i $env:Temp\chef-installer-<%= MU.chefVersion %>.msi ALLUSERS=1 /le $env:Temp\chef-client-install.log /qn" -Wait -Passthru).ExitCode - Set-Content "c:/mu_installed_chef" "yup" - } - - <% if !$mu.skipApplyUpdates %> - If (!(Test-Path "c:/mu-installer-ran-updates")){ - log "Applying Windows updates" - Import-Module PSWindowsUpdate - Get-WUInstall -AcceptAll -IgnoreReboot - Start-Sleep -s 60 - If (Test-Path "HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/WindowsUpdate/Auto Update/RebootRequired"){ - log "Registry fiddling says I need a reboot" - $need_reboot = $TRUE - } - } - <% end %> - - log "Fetching Mu deploy secret from s3://<%= MU.adminBucketName %>/<%= $mu.muID %>-secret" - aws.cmd s3 cp s3://<%= MU.adminBucketName %>/<%= $mu.muID %>-secret $env:Temp/<%= $mu.muID %>-secret - - log "Encrypting Mu deploy secret" - $deploy_secret = & "c:\opscode\chef\embedded\bin\ruby" -ropenssl -rbase64 -e "key = OpenSSL::PKey::RSA.new(Base64.urlsafe_decode64('<%= $mu.deployKey %>'))" -e "print Base64.urlsafe_encode64(key.public_encrypt(File.read('$env:Temp/<%= $mu.muID %>-secret')))" - - if (!(Get-NetFirewallRule -DisplayName "Allow SSH" -ErrorAction SilentlyContinue)){ - log "Opening port 22 in Windows Firewall" - New-NetFirewallRule -DisplayName "Allow SSH" -Direction Inbound -LocalPort 22 -Protocol TCP -Action Allow } + } + + return $install_chef +} + +If (!(Test-Path "c:\opscode\chef\embedded\bin\ruby.exe")){ + $install_chef = $true +} else { + if (removeChef("HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*")){ + $install_chef = $true + } elseif (removeChef("HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*")) { + $install_chef = $true + } else { + $install_chef = $false + } +} -<% if $mu.windowsAdminName %> - if ((Get-WmiObject win32_computersystem).partofdomain -ne $true){ - if ("$admin_username" -ne "<%= $mu.windowsAdminName %>"){ - log "Changing local admin account from $admin_username to <%= $mu.windowsAdminName %>" - ([adsi]("WinNT://./$admin_username, user")).psbase.rename("<%= $mu.windowsAdminName %>") - $need_reboot = $TRUE - } - } +If ($install_chef){ + log "Installing Chef <%= MU.chefVersion %>" + If (!(Test-Path $env:Temp/chef-installer-<%= MU.chefVersion %>.msi)){ + log "Downloading Chef installer" + $WebClient.DownloadFile("https://www.chef.io/chef/download?p=windows&pv=2012&m=x86_64&v=<%= MU.chefVersion %>","$env:Temp/chef-installer-<%= MU.chefVersion %>.msi") + } + log "Running Chef installer" + (Start-Process -FilePath msiexec -ArgumentList "/i $env:Temp\chef-installer-<%= MU.chefVersion %>.msi ALLUSERS=1 /le $env:Temp\chef-client-install.log /qn" -Wait -Passthru).ExitCode + Set-Content "c:/mu_installed_chef" "yup" +} + +<% if !$mu.skipApplyUpdates %> +If (!(Test-Path "c:/mu-installer-ran-updates")){ + log "Applying Windows updates" + Import-Module PSWindowsUpdate + Get-WUInstall -AcceptAll -IgnoreReboot + Start-Sleep -s 60 + If (Test-Path "HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/WindowsUpdate/Auto Update/RebootRequired"){ + log "Windows Update reboot" + $need_reboot = $TRUE + } +} <% end %> +fetchSecret("<%= $mu.muID %>-secret") +log "Encrypting Mu deploy secret" +$deploy_secret = & "c:\opscode\chef\embedded\bin\ruby" -ropenssl -rbase64 -e "key = OpenSSL::PKey::RSA.new(Base64.urlsafe_decode64('<%= $mu.deployKey %>'))" -e "print Base64.urlsafe_encode64(key.public_encrypt(File.read('$tmp\<%= $mu.muID %>-secret')))" + +function callMomma([string]$act) +{ + $params = @{mu_id='<%= $mu.muID %>';mu_resource_name='<%= $mu.resourceName %>';mu_resource_type='<%= $mu.resourceType %>';mu_instance_id="$awsid";mu_user='<%= $mu.muUser %>';mu_deploy_secret="$deploy_secret";$act="1"} + log "Calling Momma Cat at https://<%= $mu.publicIP %>:2260 with $act" + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} # XXX + $resp = Invoke-WebRequest -Uri https://<%= $mu.publicIP %>:2260 -Method POST -Body $params + return $resp.Content +} + +$credstr = callMomma "mu_windows_admin_creds" +$creds = $false +if($credstr){ + $credparts = $credstr.Split(";", 2) + $creds = New-Object System.Management.Automation.PSCredential($credparts[0], (ConvertTo-SecureString $credparts[1] -AsPlainText -Force)) + if($creds){ + log "Setting $admin_username password" + (([adsi]("WinNT://./$admin_username, user")).psbase.invoke("SetPassword", $credparts[1])) + } +} + <% if $mu.windowsAdminName %> - log "Creating $cygwin_dir/home/<%= $mu.windowsAdminName %>/.ssh/authorized_keys" - New-Item $cygwin_dir/home/<%= $mu.windowsAdminName %>/.ssh/authorized_keys -type file -force -value "<%= $mu.deploySSHKey %>" -<% else %> - log "Creating $cygwin_dir/home/$admin_username/.ssh/authorized_keys" - New-Item $cygwin_dir/home/$admin_username/.ssh/authorized_keys -type file -force -value "<%= $mu.deploySSHKey %>" +if ((Get-WmiObject win32_computersystem).partofdomain -ne $true){ + if ("$admin_username" -ne "<%= $mu.windowsAdminName %>"){ + log "Changing local admin account from $admin_username to <%= $mu.windowsAdminName %>" + ([adsi]("WinNT://./$admin_username, user")).psbase.rename("<%= $mu.windowsAdminName %>") + $need_reboot = $TRUE + } +} <% end %> - if((Get-WURebootStatus -Silent) -eq $true){ - log "Get-WURebootStatus telling me I need a reboot for real" - $need_reboot = $TRUE - } - - if ($need_reboot){ - log "----- REBOOT -----" - Restart-Computer -Force - exit - } else { - Add-Content c:/mu-installer-ran-updates "$(Get-Date -f MM-dd-yyyy_HH:mm:ss)" - - log "Enabling sshd service" - sleep 30; Start-Service sshd - Set-Service sshd -startuptype "Automatic" - #Get-WUInstall -AcceptAll -AutoReboot - - $url = 'https://<%= $mu.publicIP %>:2260' - log "Calling home to $url" - Start-Process -FilePath "c:\bin\cygwin\bin\curl.exe" -ArgumentList "-k --data mu_id='<%= $mu.muID %>' --data mu_resource_name='<%= $mu.resourceName %>' --data mu_resource_type='<%= $mu.resourceType %>' --data mu_instance_id='$instanceid' --data mu_bootstrap='1' --data mu_user='<%= $mu.muUser %>' --data mu_deploy_secret='$deploy_secret' $url" -Wait - log $(Get-Content $cygwin_dir/var/log/sshd.log) - } - - Set-Content "c:/mu_userdata_complete" "yup" - Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Undefined +if((Get-WURebootStatus -Silent) -eq $true){ + log "Get-WURebootStatus says to reboot" + $need_reboot = $TRUE +} + + +$muca = importCert "Mu_CA.pem" "Root" + +$myname = "<%= $mu.muID %>-<%= $mu.resourceName.upcase %>" + +$nodecert = importCert "$myname.pfx" "My" +$thumb = $nodecert.Thumbprint +# XXX Clumsy- should guard removal for mismatched hostnames/thumbprints +# actually, should remove janky certificates outright +winrm delete winrm/config/Listener?Address=*+Transport=HTTPS +winrm create winrm/config/Listener?Address=*+Transport=HTTPS "@{Hostname=`"$myname`";CertificateThumbprint=`"$thumb`"}" +$ingroup = net localgroup WinRMRemoteWMIUsers__ | Where-Object {$_ -eq "<%= $mu.windowsAdminName %>"} +if($ingroup -ne "<%= $mu.windowsAdminName %>"){ + net localgroup WinRMRemoteWMIUsers__ /add <%= $mu.windowsAdminName %> +} + +$winrmcert = importCert "$myname-winrm.crt" "TrustedPeople" +Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true +Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name LocalAccountTokenFilterPolicy -Value 1 +log $creds +if($creds){ + log "Enabling WinRM cert auth for <%= $mu.windowsAdminName %>" + New-Item -Path WSMan:\localhost\ClientCertificate -Subject '<%= $mu.windowsAdminName %>@localhost' -URI * -Issuer $muca.Thumbprint -Force -Credential $creds +} +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="8192"}' +winrm set winrm/config '@{MaxTimeoutms="1800000"}' + +if (!(Get-NetFirewallRule -DisplayName "Allow SSH" -ErrorAction SilentlyContinue)){ + log "Opening port 22 in Windows Firewall" + New-NetFirewallRule -DisplayName "Allow SSH" -Direction Inbound -LocalPort 22 -Protocol TCP -Action Allow +} +if (!(Get-NetFirewallRule -DisplayName "Allow WinRM" -ErrorAction SilentlyContinue)){ + New-NetFirewallRule -DisplayName "Allow WinRM" -Direction Inbound -LocalPort 5986 -Protocol TCP -Action Allow +} + +if ($need_reboot){ + log "- REBOOT -" + Restart-Computer -Force + exit +} else { + Add-Content c:/mu-installer-ran-updates "$(Get-Date -f MM-dd-yyyy_HH:mm:ss)" + + callMomma "mu_bootstrap" +} + +Set-Content "c:/mu_userdata_complete" "yup" +Remove-Item -Recurse $tmp +Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Undefined true