bin/mu-configure
#!/usr/local/ruby-current/bin/ruby
# Copyright:: Copyright (c) 2017 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 'optimist'
require 'simple-password-gen'
require 'socket'
require 'open-uri'
require 'colorize'
require 'timeout'
require 'etc'
require 'json'
require 'pp'
require 'readline'
require 'fileutils'
require 'erb'
require 'tmpdir'
AMROOT = Process.uid == 0
HOMEDIR = Etc.getpwuid(Process.uid).dir
CLEAN_ENV={
"PATH" => "/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/ruby-current/bin",
"HOME" => HOMEDIR
}
CLEAN_ENV_STR = CLEAN_ENV.keys.map { |k|
k+"=\""+CLEAN_ENV[k]+"\""
}.join(" ")
CHEF_CLIENT="/opt/chef/bin/chef-client"
CHEF_CTL="/opt/opscode/bin/chef-server-ctl"
GIT_PATTERN = /(((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?))?([\w\.@\:\/\-~]+)(\.git)?(\/)?/
#def _x(cmd)
# puts "#{CLEAN_ENV} #{cmd}".bold
# %x{#{CLEAN_ENV} #{cmd}}
#end
def _system(cmd)
puts cmd.bold
system(CLEAN_ENV, cmd)
end
$IN_GEM = false
gemwhich = %x{gem which mu 2>&1}.chomp
gemwhich = nil if $?.exitstatus != 0
mypath = File.realpath(File.expand_path(File.dirname(__FILE__)))
if !mypath.match(/^\/opt\/mu/)
if Gem.paths and Gem.paths.home and
(mypath.match(/^#{Gem.paths.home}/) or gemwhich.match(/^#{Gem.paths.home}/))
$IN_GEM = true
elsif $?.exitstatus == 0 and gemwhich and !gemwhich.empty?
$LOAD_PATH.each { |path|
if path.match(/\/cloud-mu-[^\/]+\/modules/) or
path.match(/#{Regexp.quote(gemwhich)}/)
$IN_GEM = true
end
}
end
end
if !$NOOP
$IN_AWS = false
begin
Timeout.timeout(2) do
instance_id = URI.open("http://169.254.169.254/latest/meta-data/instance-id").read
$IN_AWS = true if !instance_id.nil? and instance_id.size > 0
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH
end
$IN_GOOGLE = false
begin
Timeout.timeout(2) do
instance_id = URI.open(
"http://metadata.google.internal/computeMetadata/v1/instance/name",
"Metadata-Flavor" => "Google"
).read
$IN_GOOGLE = true if !instance_id.nil? and instance_id.size > 0
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH
end
$IN_AZURE = false
begin
Timeout.timeout(2) do
instance = URI.open("http://169.254.169.254/metadata/instance/compute?api-version=2017-08-01","Metadata"=>"true").read
$IN_AZURE = true if !instance.nil? and instance.size > 0
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH, Errno::EHOSTUNREACH
end
end
$possible_addresses = []
$impossible_addresses = ['127.0.0.1', 'localhost']
begin
sys_name = Socket.gethostname
official, aliases = Socket.gethostbyname(sys_name)
$possible_addresses << sys_name
$possible_addresses << official
$possible_addresses.concat(aliases)
rescue SocketError
# don't let them use the default hostname if it doesn't resolve
$impossible_addresses << sys_name
end
Socket.getifaddrs.each { |iface|
if iface.addr and iface.addr.ipv4?
$possible_addresses << iface.addr.ip_address
begin
addrinfo = Socket.gethostbyaddr(iface.addr.ip_address.split(/\./).map { |o| o.to_i }.pack("CCCC"))
$possible_addresses << addrinfo.first if !addrinfo.first.nil?
rescue SocketError
# usually no name to look up; that's ok
end
end
}
if $IN_AWS
["local-ipv4", "public-ipv4"].each { |addr|
ip = URI.open("http://169.254.169.254/latest/meta-data/#{addr}").read.chomp
$possible_addresses.unshift(ip) if ip and ip =~ /^\d+\.\d+\.\d+\.\d+/
}
elsif $IN_GOOGLE
["ip", "access-configs/0/external-ip"].each { |addr|
ip = URI.open(
"http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/#{addr}",
"Metadata-Flavor" => "Google"
).read.chomp
$possible_addresses.unshift(ip) if ip and ip =~ /^\d+\.\d+\.\d+\.\d+/
}
elsif $IN_AZURE
["privateIpAddress", "publicIpAddress"].each { |addr|
ip = URI.open("http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/#{addr}?api-version=2017-08-01&format=text","Metadata"=>"true").read
$possible_addresses.unshift(ip) if ip and ip =~ /^\d+\.\d+\.\d+\.\d+/
}
end
$possible_addresses.uniq!
$possible_addresses.reject! { |i| i.match(/^(0\.0\.0\.0$|169\.254\.|127\.0\.)/)}
# Top-level keys in $MU_CFG for which we'll provide interactive, menu-driven
# configuration.
$CONFIGURABLES = {
"public_address" => {
"title" => "Public Address",
"desc" => "IP address or hostname",
"required" => true,
"pattern" => /^(#{$impossible_addresses.map { |a| Regexp.quote(a) }.join("|") })$/,
"negate_pattern" => true,
"changes" => ["389ds", "chef-server", "chefrun", "chefcerts"]
},
"mu_admin_email" => {
"title" => "Admin Email",
"desc" => "Administative contact email",
"pattern" => /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i,
"required" => true,
"changes" => ["mu-user", "chefrun"]
},
"mu_admin_name" => {
"title" => "Admin Name",
"desc" => "Administative contact's full name",
"default" => "Mu Administrator",
"changes" => ["mu-user", "chefrun"]
},
"hostname" => {
"title" => "Local Hostname",
"pattern" => /^[a-z0-9\-_]+$/i,
"required" => true,
"rootonly" => true,
"desc" => "The local system's value for HOSTNAME",
"changes" => ["chefrun", "hostname"]
},
"disable_mommacat" => {
"title" => "Disable Momma Cat",
"default" => false,
"desc" => "Disable the Momma Cat grooming daemon. Nodes which require asynchronous Ansible/Chef bootstraps will not function. This option is only honored in gem-based installations.",
"boolean" => true
},
"adopt_change_notify" => {
"title" => "Adoption Change Notifications",
"subtree" => {
"slack" => {
"title" => "Send to Slack",
"desc" => "Report modifications to adopted resources, detected by mu-adopt --diff, to the Slack webhook and channel configured under Slack Configuration.",
"boolean" => true
},
"slack_snippet_threshold" => {
"title" => "Attachment Threshold",
"desc" => "If a list of details about a modified resources is longer than this number of lines (in JSON), it will be sent as an \"attachment,\" which in Slack means a blockquote that displays a few lines with a \"Show more\" button. The internal default is 5 lines."
},
# "email" => {
# "title" => "Send Email",
# "desc" => "",
# "boolean" => true
# }
}
},
"adopt_scrub_mu_isms" => {
"title" => "Scrub Mu-isms from Baskets of Kittens",
"default" => false,
"desc" => "Ordinarily, Mu will automatically name, tag and generate auxiliary resources in a standard Mu-ish fashion that allows for deployment of multiple clones of a given stack. Toggling this flag will change the default behavior of mu-adopt, when it creates stack descriptors from found resources, to enable or disable this behavior (see also mu-adopt's --scrub option).",
"boolean" => true
},
"slack" => {
"title" => "Slack Configuration",
"subtree" => {
"webhook" => {
"title" => "Webhook",
"desc" => "The hooks.slack.com URL for the webook to which we'll send deploy notifications"
},
"channel" => {
"title" => "Channel",
"desc" => "The channel name (without leading #) to which alerts should be sent."
}
}
},
"mommacat_port" => {
"title" => "Momma Cat Listen Port",
"pattern" => /^[0-9]+$/i,
"default" => 2260,
"required" => $IN_GEM,
"desc" => "Listen port for the Momma Cat grooming daemon",
"changes" => ["chefrun"]
},
"banner" => {
"title" => "Banner",
"desc" => "Login banner, displayed in various locations",
"changes" => ["chefrun"]
},
"mu_repository" => {
"title" => "Mu Tools Repository",
"desc" => "Source repository for Mu tools",
"pattern" => GIT_PATTERN,
"callback" => :cloneGitRepo,
"changes" => ["chefartifacts", "chefrun"],
"default" => "git://github.com/cloudamatic/mu.git"
},
"repos" => {
"title" => "Additional Repositories",
"desc" => "Optional platform repositories, as a Git URL or Github repo name (ex: eGT-Labs/fema_platform.git)",
"pattern" => GIT_PATTERN,
"callback" => :cloneGitRepo,
"changes" => ["chefartifacts", "chefrun"],
"array" => true,
"default" => ['https://github.com/cloudamatic/mu_demo_platform']
},
"master_runlist_extras" => {
"title" => "Mu Master Runlist Extras",
"desc" => "Optional extra Chef roles or recipes to invoke when running chef-client on this Master (ex: recipe[mycookbook::mumaster])",
"array" => true,
"rootonly" => true,
"changes" => ["chefrun"]
},
"allow_invade_foreign_vpcs" => {
"title" => "Invade Foreign VPCs?",
"desc" => "If set to true, Mu will be allowed to modify routing and peering behavior of VPCs which it did not create, but for which it has permissions.",
"boolean" => true
},
"ansible_dir" => {
"title" => "Ansible directory",
"desc" => "Intended for use with minimal installs which use Ansible as a groomer and which do not store Ansible artifacts in a dedicated git repository. This allows simply pointing to a local directory.",
"required" => false
},
"aws" => {
"title" => "Amazon Web Services",
"named_subentries" => true,
"subtree" => {
"account_number" => {
"title" => "Default Target Account",
"desc" => "Default target account for resources managed using these credentials. This is an AWS account number, e.g. 918972669773. If not specified, we will use the account number which owns these API keys.",
"pattern" => /^\d+$/
},
"region" => {
"title" => "Default Region",
"desc" => "Default Amazon Web Services region in which these credentials should operate"
},
"credentials" => {
"title" => "Credentials Vault:Item",
"desc" => "A secure Chef vault and item from which to retrieve an AWS access key and secret. The vault item should have 'access_key' and 'access_secret' elements."
},
"credentials_file" => {
"title" => "Credentials File",
"desc" => "An INI-formatted AWS credentials file, of the type used by the AWS command-line tools. This is less secure than using 'credentials' to store these in a Chef vault. See: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
},
"access_key" => {
"title" => "Access Key",
"desc" => "Credentials used for accessing the AWS API (looks like: AKIAINWLOOAA24PBRBZA)",
"pattern" => /^[a-z0-9]+$/i
},
"access_secret" => {
"title" => "Access Secret",
"desc" => "Credentials used for accessing the AWS API (looks like: +Z16iRP9QAq7EcjHINyEMs3oR7A76QpfaSgCBogp)."
},
"log_bucket_name" => {
"title" => "Log and Secret Bucket Name",
"desc" => "S3 bucket into which we'll synchronize deploy secrets, and if we're hosted in AWS, collected system logs",
"required" => true,
"changes" => ["chefrun"]
},
"default" => {
"title" => "Is Default",
"default" => false,
"desc" => "If set to true, Mu will default to these AWS credentials when targeting AWS resources",
"boolean" => true
}
}
},
"google" => {
"title" => "Google Cloud Platform",
"named_subentries" => true,
"subtree" => {
"project" => {
"title" => "Default Project",
"required" => true,
"desc" => "Default Google Cloud Platform project in which we operate and deploy. Generate a service account at: https://console.cloud.google.com/iam-admin/serviceaccounts/project, making sure the account has sufficient privileges to manage cloud resources. Download the private key as JSON, and import that key to the vault specified here. Import example: knife vault create secrets google -J my-google-service-account.json"
},
"credentials" => {
"title" => "Credentials Vault:Item",
"desc" => "A secure Chef vault and item from which to retrieve the JSON-formatted Service Account credentials for our GCP account, in the format vault:itemname (e.g. 'secrets:google'). Generate a service account at: https://console.cloud.google.com/iam-admin/serviceaccounts/project, making sure the account has sufficient privileges to manage cloud resources. Download the private key as JSON, and import that key to the vault specified here. Import example: knife vault create secrets google -J my-google-service-account.json "
},
"credentials_file" => {
"title" => "Credentials File",
"desc" => "JSON-formatted Service Account credentials for our GCP account, stored in plain text in a file. Generate a service account at: https://console.cloud.google.com/iam-admin/serviceaccounts/project, making sure the account has sufficient privileges to manage cloud resources. Download the private key as JSON and point this argument to the file. This is less secure than using 'credentials' to store in a vault."
},
"credentials_encoded" => {
"title" => "Base64-Encoded Credentials",
"desc" => "JSON-formatted Service Account credentials for our GCP account, b64-encoded and dropped directly into mu.yaml. Generate a service account at: https://console.cloud.google.com/iam-admin/serviceaccounts/project, making sure the account has sufficient privileges to manage cloud resources. Download the private key as JSON and point this argument to the file. This is less secure than using 'credentials' to store in a vault."
},
"region" => {
"title" => "Default Region",
"desc" => "Default Google Cloud Platform region in which we operate and deploy",
"default" => "us-east4"
},
"log_bucket_name" => {
"title" => "Log and Secret Bucket Name",
"desc" => "Cloud Storage bucket into which we'll synchronize deploy secrets, and if we're hosted in GCP, collected system logs",
"required" => true,
"changes" => ["chefrun"]
},
"masequerade_as" => {
"title" => "GSuite Masquerade User",
"required" => false,
"desc" => "For Google Cloud projects which are attached to a GSuite domain. GCP service accounts cannot view or manage GSuite resources (groups, users, etc) directly, but must instead masquerade as a GSuite user which has delegated authority to the service account. See also: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority"
},
"org" => {
"title" => "Default Org/Domain",
"desc" => "For credential sets which have access to multiple GSuite or Cloud Identity orgs, you must specify a default organization (e.g. my.domain.com)."
},
"customer_id" => {
"title" => "GSuite Customer ID",
"required" => false,
"desc" => "For Google Cloud projects which are attached to a GSuite domain. Some API calls (groups, users, etc) require this identifier. From admin.google.com, choose Security, the Single Sign On, and look for the Entity ID field. The value after idpid= in the URL there should be the customer ID."
},
"ignore_habitats" => {
"title" => "Ignore These Projects",
"desc" => "Optional list of projects to ignore, for credentials which have visibility into multiple projects",
"array" => true
},
"restrict_to_habitats" => {
"title" => "Operate On Only These Projects",
"desc" => "Optional list of projects to which we'll restrict all of our activities.",
"array" => true
},
"default" => {
"title" => "Is Default Account",
"default" => false,
"desc" => "If set to true, Mu will use this set of GCP credentials when targeting the Google Cloud without a specific account having been requested",
"boolean" => true
}
}
},
"azure" => {
"title" => "Microsoft Azure Cloud Computing Platform & Services",
"named_subentries" => true,
"subtree" => {
"directory_id" => {
"title" => "Directory ID",
"desc" => "AKA Tenant ID; the default Microsoft Azure Directory project in which we operate and deploy, from https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview"
},
"client_id" => {
"title" => "Client ID",
"desc" => "App client id used to authenticate to our subscription. From https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview"
},
"client_secret" => {
"title" => "Client Secret",
"desc" => "App client secret used to authenticate to our subscription. From https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview under the 'Certificates & secrets' tab, 'Client secrets.' This can only be retrieved upon initial secret creation."
},
"subscription" => {
"title" => "Default Subscription",
"desc" => "Default Microsoft Azure Subscription we will use to deploy, from https://portal.azure.com/#blade/Microsoft_Azure_Billing/SubscriptionsBlade"
},
# "credentials" => {
# "title" => "Credentials Vault:Item",
# "desc" => "A secure Chef vault and item from which to retrieve the JSON-formatted Service Account credentials for our Azure account, in the format vault:itemname (e.g. 'secrets:google'). Generate a service account at: https://console.cloud.google.com/iam-admin/serviceaccounts/project, making sure the account has sufficient privileges to manage cloud resources. Download the private key as JSON, and import that key to the vault specified here. Import example: knife vault create secrets google -J my-google-service-account.json "
# },
"credentials_file" => {
"title" => "Credentials File",
"desc" => "JSON file which contains a hash of directory_id, client_id, client_secret, and subscription values. If found, these will be override values entered directly in mu-configure."
},
"region" => {
"title" => "Default Region",
"desc" => "Default Microsoft Azure region in which we operate and deploy",
"default" => "eastus"
},
# "log_bucket_name" => {
# "title" => "Log and Secret Bucket Name",
# "desc" => "Cloud Storage bucket into which we'll synchronize deploy secrets, and if we're hosted in Azure, collected system logs",
# "changes" => ["chefrun"]
# },
"default" => {
"title" => "Is Default Account",
"default" => false,
"desc" => "If set to true, Mu will use this set of Azure credentials when targeting Azure without a specific account having been requested",
"boolean" => true
}
}
}
}
def cloneHash(hash)
new = {}
hash.each_pair { |k,v|
if v.is_a?(Hash)
new[k] = cloneHash(v)
elsif !v.nil?
new[k] = v.dup
end
}
new
end
# Load values from our existing configuration into the $CONFIGURABLES hash
def importCurrentValues
require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
$CONFIGURABLES.each_key { |key|
next if !$MU_CFG.has_key?(key)
if $CONFIGURABLES[key].has_key?("subtree")
# It's a sub-tree. I'm too lazy to write a recursive thing for this, just
# cover the simple case that we actually care about for now.
if $CONFIGURABLES[key]["named_subentries"]
$CONFIGURABLES[key]['subtree']["#title"] = $CONFIGURABLES[key]['title']
$MU_CFG[key].each_pair { |nameentry, subtree|
$CONFIGURABLES[key]['subtree']["#entries"] ||= {}
$CONFIGURABLES[key]['subtree']["#entries"][nameentry] = cloneHash($CONFIGURABLES[key]['subtree'])
$CONFIGURABLES[key]['subtree']["#entries"][nameentry].delete("#entries")
$CONFIGURABLES[key]["subtree"]["#entries"][nameentry]["name"] = {
"title" => "Name",
"desc" => "A name/alias for this account.",
"required" => true,
"value" => nameentry
}
$CONFIGURABLES[key]["subtree"].keys.each { |subkey|
next if !subtree.has_key?(subkey)
$CONFIGURABLES[key]["subtree"]["#entries"][nameentry][subkey]["value"] = subtree[subkey]
}
}
else
$CONFIGURABLES[key]["subtree"].keys.each { |subkey|
next if !$MU_CFG[key].has_key?(subkey)
$CONFIGURABLES[key]["subtree"][subkey]["value"] = $MU_CFG[key][subkey]
}
end
else
$CONFIGURABLES[key]["value"] = $MU_CFG[key]
end
}
end
if !$NOOP
$opts = Optimist::options do
banner <<-EOS
EOS
required = []
opt :noninteractive, "Skip menu-based configuration prompts. If there is no existing configuration, the following flags are required: #{required.map{|x|"--"+x}.join(", ")}", :require => false, :default => false, :type => :boolean
$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']
subdata['cli-opt'] = (key+"-"+subkey).gsub(/_/, "-")
opt subdata['cli-opt'].to_sym, subdata["desc"], :require => false, :type => (subdata["boolean"] ? :boolean : :string)
required << subdata['cli-opt'] if subdata['required']
}
elsif data["array"]
data['cli-opt'] = key.gsub(/_/, "-")
opt data['cli-opt'].to_sym, data["desc"], :require => false, :type => (data["boolean"] ? :booleans : :strings)
required << data['cli-opt'] if data['required']
else
data['cli-opt'] = key.gsub(/_/, "-")
opt data['cli-opt'].to_sym, data["desc"], :require => false, :type => (data["boolean"] ? :boolean : :string)
required << data['cli-opt'] if data['required']
end
}
opt :force, "Run all rebuild actions, whether or not our configuration is changed.", :require => false, :default => false, :type => :boolean if AMROOT
opt :ssh_keys, "One or more paths to SSH private keys, which we can try to use for SSH-based Git clone operations", :require => false, :type => :strings
end
if ENV.has_key?("MU_INSTALLDIR")
MU_BASE = ENV["MU_INSTALLDIR"]
else
MU_BASE = "/opt/mu"
end
def cfgPath
home = Etc.getpwuid(Process.uid).dir
username = Etc.getpwuid(Process.uid).name
if Process.uid == 0
if ENV.include?('MU_INSTALLDIR')
ENV['MU_INSTALLDIR']+"/etc/mu.yaml"
elsif Dir.exist?("/opt/mu")
"/opt/mu/etc/mu.yaml"
else
"#{home}/.mu.yaml"
end
else
"#{home}/.mu.yaml"
end
end
$INITIALIZE = (!File.size?(cfgPath) or $opts[:force])
$HAVE_GLOBAL_CONFIG = File.size?("#{MU_BASE}/etc/mu.yaml")
if !AMROOT and !$HAVE_GLOBAL_CONFIG and !$IN_GEM and Dir.exist?("/opt/mu/lib")
puts "Global configuration has not been initialized or is missing. Must run as root to correct."
exit 1
end
if !$HAVE_GLOBAL_CONFIG and $opts[:noninteractive] and (!$opts[:"public-address"] or !$opts[:"mu-admin-email"])
if $IN_GEM
importCurrentValues # maybe we're in local-only mode
end
if !$MU_CFG or !$MU_CFG['mu_admin_email'] or !$MU_CFG['mu_admin_name']
puts "Specify --public-address and --mu-admin-email on new non-interactive configs"
exit 1
end
end
if AMROOT and !$IN_GEM
Dir.chdir("/")
if $IN_AWS
_system("#{MU_BASE}/lib/bin/mu-aws-setup --optdisk")
elsif $IN_GOOGLE
_system("#{MU_BASE}/lib/bin/mu-gcp-setup --optdisk")
elsif $IN_AZURE
_system("#{MU_BASE}/lib/bin/mu-azure-setup --optdisk")
end
exit 1 if $?.exitstatus != 0
end
_system("cd #{MU_BASE}/lib/modules && umask 0022 && /usr/local/ruby-current/bin/bundle install")
_system("cd #{MU_BASE}/lib/modules && umask 0022 && /opt/chef/embedded/bin/bundle install")
KNIFE_TEMPLATE = "log_level :info
log_location STDOUT
node_name '<%= chefuser %>'
client_key '<%= MU_BASE %>/var/users/<%= user %>/<%= chefuser %>.user.key'
validation_client_name 'mu-validator'
validation_key '<%= MU_BASE %>/var/orgs/<%= user %>/<%= chefuser %>.org.key'
chef_server_url 'https://<%= MU.mu_public_addr %>:7443/organizations/<%= chefuser %>'
chef_server_root 'https://<%= MU.mu_public_addr %>:7443/organizations/<%= chefuser %>'
syntax_check_cache_path '<%= HOMEDIR %>/.chef/syntax_check_cache'
cookbook_path [ '<%= HOMEDIR %>/.chef/cookbooks', '<%= HOMEDIR %>/.chef/site_cookbooks' ]
<% if $MU_CFG.has_key?('ssl') and $MU_CFG['ssl'].has_key?('chain') %>
ssl_ca_path '<%= File.dirname($MU_CFG['ssl']['chain']) %>'
ssl_ca_file '<%= File.basename($MU_CFG['ssl']['chain']) %>'
<% end %>
knife[:vault_mode] = 'client'
knife[:vault_admins] = ['<%= chefuser %>']"
CLIENT_TEMPLATE = "chef_server_url 'https://<%= MU.mu_public_addr %>:7443/organizations/<%= user %>'
validation_client_name 'mu-validator'
log_location STDOUT
node_name 'MU-MASTER'
chef_license 'accept'
verify_api_cert false
ssl_verify_mode :verify_none
"
#chef_server_url "https://127.0.0.1:7443/organizations/mu"
#validation_client_name "mu-validator"
#chef_license "accept"
#log_location STDOUT
#node_name "MU-MASTER"
#verify_api_cert false
#ssl_verify_mode :verify_none
#trusted_certs_dir "/etc/chef/trusted_certs"
#file_cache_path "/var/chef/cache"
#file_backup_path "/var/chef/backup"
PIVOTAL_TEMPLATE = "node_name 'pivotal'
chef_server_url 'https://<%= MU.mu_public_addr %>:7443'
chef_server_root 'https://<%= MU.mu_public_addr %>:7443'
no_proxy '<%= MU.mu_public_addr %>'
client_key '/etc/opscode/pivotal.pem'
ssl_verify_mode :verify_none
"
$CHANGES = []
$MENU_MAP = {}
def assignMenuEntries(tree = $CONFIGURABLES, map = $MENU_MAP)
count = 1
tree.each_pair { |key, data|
next if !data.is_a?(Hash)
next if !AMROOT and data['rootonly']
if data.has_key?("subtree")
letters = ("a".."z").to_a
lettercount = 0
if data['named_subentries']
# Generate a stub entry for adding a new item
map[count.to_s] = cloneHash(data["subtree"])
map[count.to_s].each_pair { |k, v| v.delete("value") } # use defaults
map[count.to_s]["name"] = {
"title" => "Name",
"desc" => "A name/alias for this account.",
"required" => true
}
map[count.to_s]["#addnew"] = true
map[count.to_s]["#title"] = data['title']
map[count.to_s]["#key"] = key
# Now the menu entries for the existing ones
if data['subtree']['#entries']
data['subtree']['#entries'].each_pair { |nameentry, subdata|
next if data['readonly']
next if !subdata.is_a?(Hash)
subdata["#menu"] = count.to_s+letters[lettercount]
subdata["#title"] = nameentry
subdata["#key"] = key
subdata["#entries"] = cloneHash(data["subtree"]["#entries"])
subdata["is_submenu"] = true
map[count.to_s+letters[lettercount]] = tree[key]["subtree"]['#entries'][nameentry]
map[count.to_s+letters[lettercount]]['#entries'] ||= cloneHash(data["subtree"]["#entries"])
lettercount = lettercount + 1
}
end
else
data["subtree"].each_pair { |subkey, subdata|
next if !AMROOT and subdata['rootonly']
tree[key]["subtree"][subkey]["#menu"] = count.to_s+letters[lettercount]
tree[key]["subtree"][subkey]["#key"] = subkey
map[count.to_s+letters[lettercount]] = tree[key]["subtree"][subkey]
lettercount = lettercount + 1
}
end
end
tree[key]["#menu"] = count.to_s
tree[key]["#key"] = key
map[count.to_s] ||= tree[key]
count = count + 1
}
map#.freeze
end
def trySSHKeyWithGit(repo, keypath = nil)
cfgbackup = nil
deletekey = false
repo.match(/^([^@]+?)@([^:]+?):/)
ssh_user = Regexp.last_match(1)
ssh_host = Regexp.last_match(2)
if keypath.nil?
response = nil
puts "Would you like to provide a private ssh key for #{repo} and try again?"
begin
response = Readline.readline("Y/N> ".bold, false)
end while !response and !response.match(/^(y|n)$/i)
if response == "y" or response == "Y"
Dir.mkdir("#{HOMEDIR}/.ssh", 0700) if !Dir.exist?("#{HOMEDIR}/.ssh")
keynamestr = repo.gsub(/[^a-z0-9\-]/i, "-") + Process.pid.to_s
keypath = "#{HOMEDIR}/.ssh/#{keynamestr}"
puts "Paste a complete SSH private key for #{ssh_user.bold}@#{ssh_host.bold} below, then ^D"
_system("cat > #{keypath}")
File.chmod(0600, keypath)
puts "Key saved to "+keypath.bold
deletekey = true
else
return false
end
end
if File.exist?("#{HOMEDIR}/.ssh/config")
FileUtils.cp("#{HOMEDIR}/.ssh/config", "#{HOMEDIR}/.ssh/config.bak.#{Process.pid.to_s}")
cfgbackup = "#{HOMEDIR}/.ssh/config.bak.#{Process.pid.to_s}"
end
File.open("#{HOMEDIR}/.ssh/config", "a", 0600){ |f|
f.puts "Host "+ssh_host
f.puts " User "+ssh_user
f.puts " IdentityFile "+keypath
f.puts " StrictHostKeyChecking no"
}
puts "/usr/bin/git clone #{repo}"
output = %x{/usr/bin/git clone #{repo} 2>&1}
if $?.exitstatus == 0
puts "Successfully cloned #{repo}".green.on_black
return true
else
puts output.red.on_black
if cfgbackup
puts "Restoring #{HOMEDIR}/.ssh/config"
File.rename(cfgbackup, "#{HOMEDIR}/.ssh/config")
end
if deletekey
puts "Removing #{keypath}"
File.unlink(keypath)
end
end
return false
end
def cloneGitRepo(repo)
puts "Testing ability to check out Git repository #{repo.bold}"
fullrepo = repo
if !repo.match(/@|:\/\//) # we try ssh first
fullrepo = "git@github.com:"+repo
puts "Doesn't look like a full URL, trying SSH to #{fullrepo}"
end
cwd = Dir.pwd
Dir.mktmpdir("mu-git-test-") { |dir|
Dir.chdir(dir)
puts "/usr/bin/git clone #{fullrepo}"
output = %x{/usr/bin/git clone #{fullrepo} 2>&1}
if $?.exitstatus == 0
puts "Successfully cloned #{fullrepo}".green.on_black
Dir.chdir(cwd)
return fullrepo
elsif $?.exitstatus != 0 and output.match(/permission denied/i)
puts ""
puts output.red.on_black
if $opts[:"ssh-keys-given"]
$opts[:"ssh-keys"].each { |keypath|
if trySSHKeyWithGit(fullrepo, keypath)
Dir.chdir(cwd)
return fullrepo
end
}
end
if !$opts[:noninteractive]
if trySSHKeyWithGit(fullrepo)
Dir.chdir(cwd)
return fullrepo
end
end
end
if !repo.match(/@|:\/\//)
fullrepo = "git://github.com/"+repo
puts ""
puts "No luck there, trying #{fullrepo}".bold
puts "/usr/bin/git clone #{fullrepo}"
output = %x{/usr/bin/git clone #{fullrepo} 2>&1}
if $?.exitstatus == 0
puts "Successfully cloned #{fullrepo}".green.on_black
Dir.chdir(cwd)
return fullrepo
else
puts output.red.on_black
fullrepo = "https://github.com/"+repo
puts "Final attempt, trying #{fullrepo}"
puts "/usr/bin/git clone #{fullrepo}"
output = %x{/usr/bin/git clone #{fullrepo} 2>&1}
if $?.exitstatus == 0
puts "Successfully cloned #{fullrepo}".green.on_black
Dir.chdir(cwd)
return fullrepo
else
puts output.red.on_black
end
end
else
puts "No other methods I can think to try, giving up on #{repo.bold}".red.on_black
end
}
Dir.chdir(cwd)
nil
end
# Rustle up some sensible default values, if this is our first time
def setDefaults
ips = []
if $IN_AWS
["public-ipv4", "local-ipv4"].each { |addr|
begin
Timeout.timeout(2) do
ip = URI.open("http://169.254.169.254/latest/meta-data/#{addr}").read
ips << ip if !ip.nil? and ip.size > 0
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError
# these are ok to ignore
end
}
elsif $IN_GOOGLE
base_url = "http://metadata.google.internal/computeMetadata/v1"
begin
Timeout.timeout(2) do
# TODO iterate across multiple interfaces/access-configs
ip = URI.open("#{base_url}/instance/network-interfaces/0/ip", "Metadata-Flavor" => "Google").read
ips << ip if !ip.nil? and ip.size > 0
ip = URI.open("#{base_url}/instance/network-interfaces/0/access-configs/0/external-ip", "Metadata-Flavor" => "Google").read
ips << ip if !ip.nil? and ip.size > 0
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError => e
# This is fairly normal, just handle it gracefully
end
end
$CONFIGURABLES["allow_invade_foreign_vpcs"]["default"] = false
$CONFIGURABLES["public_address"]["default"] = $possible_addresses.first
$CONFIGURABLES["hostname"]["default"] = Socket.gethostname
$CONFIGURABLES["banner"]["default"] = "Mu Master at #{$CONFIGURABLES["public_address"]["default"]}"
if $IN_AWS
# XXX move this crap to a callback hook for puttering around in the AWS submenu
aws = JSON.parse(URI.open("http://169.254.169.254/latest/dynamic/instance-identity/document").read)
iam = nil
begin
iam = URI.open("http://169.254.169.254/latest/meta-data/iam/security-credentials").read
rescue OpenURI::HTTPError, SocketError
end
# $CONFIGURABLES["aws"]["subtree"]["account_number"]["default"] = aws["accountId"]
$CONFIGURABLES["aws"]["subtree"]["region"]["default"] = aws["region"]
if iam and iam.size > 0
# XXX can we think of a good way to test our permission set?
$CONFIGURABLES["aws"]["subtree"]["access_key"]["desc"] = $CONFIGURABLES["aws"]["subtree"]["access_key"]["desc"] + ". Not necessary if IAM Profile #{iam.bold} has sufficient API access."
$CONFIGURABLES["aws"]["subtree"]["access_secret"]["desc"] = $CONFIGURABLES["aws"]["subtree"]["access_key"]["desc"] + ". Not necessary if IAM Profile #{iam.bold} has sufficient API access."
end
end
$CONFIGURABLES["aws"]["subtree"]["log_bucket_name"]["default"] = $CONFIGURABLES["hostname"]["default"]
$CONFIGURABLES["google"]["subtree"]["log_bucket_name"]["default"] = $CONFIGURABLES["hostname"]["default"]
end
def runValueCallback(desc, val)
if desc['array']
if desc["callback"]
newval = []
val.each { |v|
v = send(desc["callback"].to_sym, v)
newval << v if !v.nil?
}
val = newval
end
elsif desc["callback"]
val = send(desc["callback"].to_sym, val)
end
val
end
def importCLIValues
$CONFIGURABLES.each_pair { |key, data|
next if !AMROOT and data['rootonly']
if data.has_key?("subtree")
if !data['named_subentries']
data["subtree"].each_pair { |subkey, subdata|
next if !AMROOT and subdata['rootonly']
if $opts[(subdata['cli-opt'].+"_given").to_sym]
newval = runValueCallback(subdata, $opts[subdata['cli-opt'].to_sym])
subdata["value"] = newval if !newval.nil?
$CHANGES.concat(subdata['changes']) if subdata['changes']
end
}
# Honor CLI adds for named trees (credentials, etc) if there are no
# entries in them yet.
elsif data["#entries"].nil? or data["#entries"].empty?
newvals = false
data["subtree"].each_pair { |subkey, subdata|
next if !AMROOT and subdata['rootonly']
next if !subdata['cli-opt']
if $opts[(subdata['cli-opt']+"_given").to_sym]
newval = runValueCallback(subdata, $opts[subdata['cli-opt'].to_sym])
if !newval.nil?
subdata["value"] = newval
newvals = true
end
end
}
if newvals
newtree = data["subtree"].dup
newtree['default']['value'] = true if newtree['default']
data['subtree']['#entries'] = {
"default" => newtree
}
end
end
else
if $opts[(data['cli-opt']+"_given").to_sym]
newval = runValueCallback(data, $opts[data['cli-opt'].to_sym])
data["value"] = newval if !newval.nil?
$CHANGES.concat(data['changes']) if data['changes']
end
end
}
end
def printVal(data)
valid = true
valid = validate(data["value"], data, false) if data["value"]
value = if data["value"] and data["value"] != ""
data["value"]
elsif data["default"] and data["default"] != ""
data["default"]
end
if data['readonly'] and value
print " - "+value.to_s.cyan.on_black
elsif !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
elsif !data["default"].nil?
print " - "+data["default"].to_s.yellow.on_black+" (DEFAULT)"
end
end
# Converts the current $CONFIGURABLES object to a Hash suitable for merging
# with $MU_CFG.
def setConfigTree(tree = $CONFIGURABLES)
cfg = $MU_CFG.nil? ? {} : $MU_CFG.dup
tree.each_pair { |key, data|
next if !AMROOT and data['rootonly']
if data.has_key?("subtree")
if data["named_subentries"]
if data["subtree"]["#entries"]
data["subtree"]["#entries"].each_pair { |name, block|
next if !block.is_a?(Hash)
block.each_pair { |subkey, subdata|
next if subkey.match(/^#/) or !subdata.is_a?(Hash)
cfg[key] ||= {}
cfg[key][name] ||= {}
cfg[key][name][subkey] = subdata['value'] if subdata['value']
}
}
end
else
data["subtree"].each_pair { |subkey, subdata|
if !subdata["value"].nil?
cfg[key] ||= {}
cfg[key][subkey] = subdata["value"]
elsif !subdata["default"].nil? and !$HAVE_GLOBAL_CONFIG or ($MU_CFG and (!$MU_CFG[key] or !$MU_CFG[key][subkey]))
cfg[key] ||= {}
cfg[key][subkey] = subdata["default"]
end
}
end
elsif !data["value"].nil?
cfg[key] = data["value"]
elsif !data["default"].nil? and !$HAVE_GLOBAL_CONFIG or ($MU_CFG and !$MU_CFG[key])
cfg[key] = data["default"]
end
}
cfg
end
def displayCurrentOpts(tree = $CONFIGURABLES)
count = 1
optlist = []
tree.each_pair { |key, data|
next if !data.is_a?(Hash)
next if !AMROOT and data['rootonly']
if data["title"].nil? or data["#menu"].nil?
next
end
print data["#menu"].bold+") "+data["title"]
if data.has_key?("subtree")
puts ""
if data["named_subentries"]
if data['subtree']['#entries']
data['subtree']['#entries'].each_pair { |nameentry, subdata|
next if nameentry.nil? or nameentry.match(/^#/)
puts " "+subdata["#menu"].bold+". "+nameentry.green.on_black
}
end
else
data["subtree"].each_pair { |subkey, subdata|
next if !AMROOT and subdata['rootonly']
print " "+subdata["#menu"].bold+". "+subdata["title"]
printVal(subdata)
puts ""
}
end
else
printVal(data)
puts ""
end
count = count + 1
}
optlist
end
###############################################################################
trap("INT"){ puts "" ; exit }
importCurrentValues if !$INITIALIZE or $HAVE_GLOBAL_CONFIG or $IN_GEM
importCLIValues
setDefaults
assignMenuEntries # populates $MENU_MAP
def ask(desc)
puts ""
puts (desc['required'] ? "REQUIRED".red.on_black : "OPTIONAL".yellow.on_black)+" - "+desc["desc"]
puts "Enter one or more values, separated by commas".yellow.on_black if desc['array']
puts "Enter 0 or false, 1 or true".yellow.on_black if desc['boolean']
prompt = desc["title"].bold + "> "
current = desc['value'] || desc['default']
if current
current = current.join(", ") if desc['array'] and current.is_a?(Array)
Readline.pre_input_hook = -> do
Readline.insert_text current.to_s
Readline.redisplay
Readline.pre_input_hook = nil
end
end
val = Readline.readline(prompt, false)
if desc['array'] and !val.nil?
val = val.strip.split(/\s*,\s*/)
end
if desc['boolean']
val = false if ["0", "false", "FALSE"].include?(val)
val = true if ["1", "true", "TRUE"].include?(val)
end
val = runValueCallback(desc, val)
val = current if val.nil? and desc['value']
val
end
def validate(newval, reqs, addnewline = true, in_use: [])
ok = true
def validate_individual_value(newval, reqs, addnewline, in_use: [])
ok = true
if reqs['boolean'] and newval != true and newval != false and newval != nil
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 in_use and in_use.size > 0 and in_use.include?(newval)
puts "\n##{reqs['title'].bold} #{newval} not available".light_red.on_black
puts "\n\n" if addnewline
ok = false
elsif reqs['pattern']
if newval.nil?
puts "\nSupplied value for #{reqs['title'].bold} did not pass validation".light_red.on_black
puts "\n\n" if addnewline
ok = false
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}' 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, addnewline, in_use: in_use)
}
end
else
ok = false if !validate_individual_value(newval, reqs, addnewline, in_use: in_use)
end
ok
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
def generateMiniMenu(srctree)
map = {}
tree = cloneHash(srctree)
return [tree, map]
end
def menu(tree = $CONFIGURABLES, map = $MENU_MAP, submenu_name = nil, in_use_names = [])
begin
optlist = displayCurrentOpts(tree)
begin
if submenu_name
print "Enter an option to change, "+"O".bold+" to save #{submenu_name.bold}, or "+"q".bold+" to return.\n> "
else
print "Enter an option to change, "+"O".bold+" to save this config, or "+"^D".bold+" to quit.\n> "
end
answer = gets
if answer.nil?
puts ""
exit 0
end
answer.strip!
rescue EOFError
puts ""
exit 0
end
if map.has_key?(answer) and map[answer]["#addnew"]
minimap = {}
assignMenuEntries(map[answer], minimap)
newtree, newmap = menu(
map[answer],
minimap,
map[answer]['#title']+" (NEW)",
if map[answer]['#entries']
map[answer]['#entries'].keys.reject { |k| k.match(/^#/) }
end
)
if newtree
newname = newtree["name"]["value"]
newtree.delete("#addnew")
parentname = map[answer]['#key']
tree[parentname]['subtree'] ||= {}
tree[parentname]['subtree']['#entries'] ||= {}
# if we're in cloud land and just added a 2nd entry, set the original
# one to 'default'
if tree[parentname]['subtree']['#entries'].size == 1
end
tree[parentname]['subtree']['#entries'][newname] = cloneHash(newtree)
map = {} # rebuild the menu map to include new entries
assignMenuEntries(tree, map)
end
# exit
# map[answer] = newtree if newtree
elsif map.has_key?(answer) and map[answer]["is_submenu"]
minimap = {}
parentname = map[answer]['#key']
entryname = map[answer]['#title']
puts PP.pp(map[answer], '').yellow
puts PP.pp(tree[parentname]['subtree']['#entries'][entryname], '').red
assignMenuEntries(tree[parentname]['subtree']['#entries'][entryname], minimap)
newtree, newmap = menu(
map[answer],
minimap,
map[answer]["#title"],
(map[answer]['#entries'].keys - [map[answer]['#title']])
)
map[answer] = newtree if newtree
elsif map.has_key?(answer) and !map[answer].has_key?("subtree")
newval = ask(map[answer])
if !validate(newval, map[answer], in_use: in_use_names)
sleep 1
next
end
map[answer]['value'] = newval == "" ? nil : newval
tree[map[answer]['#key']]['value'] = newval
$CHANGES.concat(map[answer]['changes']) if map[answer].include?("changes")
if map[answer]['title'] == "Local Hostname"
# $CONFIGURABLES["aws"]["subtree"]["log_bucket_name"]["default"] = newval
# $CONFIGURABLES["google"]["subtree"]["log_bucket_name"]["default"] = newval
elsif map[answer]['title'] == "Public Address"
$CONFIGURABLES["banner"]["default"] = "Mu Master at #{newval}"
end
changed = true
puts ""
elsif ["q", "Q"].include?(answer)
return nil
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"
return [tree, map]
end
if !$opts[:noninteractive]
$CONFIGURABLES, $MENU_MAP = menu
$MU_CFG = setConfigTree
else
$MU_CFG = setConfigTree
if !entireConfigValid?
puts "Configuration had validation errors, exiting.\nRe-invoke #{$0} to correct."
exit 1
end
end
if AMROOT
newcfg = cloneHash($MU_CFG)
require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
newcfg['multiuser'] = true
saveMuConfig(newcfg)
$MU_CFG = loadMuConfig($MU_SET_DEFAULTS)
end
def set389DSCreds
require 'mu'
credlist = {
"bind_creds" => {
"user" => "CN=mu_bind_creds,#{$MU_CFG["ldap"]['user_ou']}"
},
"join_creds" => {
"user" => "CN=mu_join_creds,#{$MU_CFG["ldap"]['user_ou']}"
},
"cfg_directory_adm" => {
"user" => "admin"
},
"root_dn_user" => {
"user" => "CN=root_dn_user"
}
}
credlist.each_pair { |creds, cfg|
begin
data = nil
if $MU_CFG["ldap"].has_key?(creds)
data = MU::Groomer::Chef.getSecret(
vault: $MU_CFG["ldap"][creds]["vault"],
item: $MU_CFG["ldap"][creds]["item"]
)
MU::Groomer::Chef.grantSecretAccess("MU-MASTER", $MU_CFG["ldap"][creds]["vault"], $MU_CFG["ldap"][creds]["item"])
else
data = MU::Groomer::Chef.getSecret(vault: "mu_ldap", item: creds)
MU::Groomer::Chef.grantSecretAccess("MU-MASTER", "mu_ldap", creds)
end
rescue MU::Groomer::MuNoSuchSecret
user = cfg["user"]
pw = Password.pronounceable(14..16)
if $MU_CFG["ldap"].has_key?(creds)
data = {
$MU_CFG["ldap"][creds]["username_field"] => user,
$MU_CFG["ldap"][creds]["password_field"] => pw
}
MU::Groomer::Chef.saveSecret(
vault: $MU_CFG["ldap"][creds]["vault"],
item: $MU_CFG["ldap"][creds]["item"],
data: data,
permissions: "name:MU-MASTER"
)
else
MU::Groomer::Chef.saveSecret(
vault: "mu_ldap",
item: creds,
data: { "username" => user, "password" => pw },
permissions: "name:MU-MASTER"
)
end
end
}
end
if AMROOT and !$IN_GEM
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 and cur_chef_version.sub(/\-\d+$/, "") != pref_chef_version) or cur_chef_version.match(/is not installed/)
puts "Updating MU-MASTER's Chef Client to '#{pref_chef_version}' from '#{cur_chef_version}'"
chef_installer = URI.open("https://omnitruck.chef.io/install.sh").read
File.open("#{HOMEDIR}/chef-install.sh", File::CREAT|File::TRUNC|File::RDWR, 0644){ |f|
f.puts chef_installer
}
_system("/bin/rm -rf /opt/chef ; sh #{HOMEDIR}/chef-install.sh -v #{pref_chef_version}");
# This will go fix gems, permissions, etc
_system("/opt/chef/bin/chef-apply #{MU_BASE}/lib/cookbooks/mu-master/recipes/init.rb");
end
end
if $INITIALIZE
if AMROOT and !$IN_GEM
%x{/sbin/service iptables stop} # Chef run will set up correct rules later
end
$MU_SET_DEFAULTS = setConfigTree
require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
saveMuConfig($MU_SET_DEFAULTS)
else
if AMROOT
$NEW_CFG = $MU_CFG.merge(setConfigTree)
else
$NEW_CFG = setConfigTree
end
saveMuConfig($NEW_CFG)
$MU_CFG = $MU_CFG.merge(setConfigTree)
require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
end
begin
require 'mu'
rescue LoadError, Gem::MissingSpecError
_system("cd #{MU_BASE}/lib/modules && umask 0022 && /usr/local/ruby-current/bin/bundle install")
require 'bundler'
pwd = Dir.pwd
Dir.chdir(MU_BASE+"/lib/modules")
Bundler.setup
require 'mu'
Dir.chdir(pwd)
rescue MU::MuError => e
puts "Correct the above error before proceeding. To retry, run:\n\n#{$0.bold} #{ARGV.join(" ").bold}"
exit 1
end
if $IN_GEM
if $INITIALIZE
$MU_CFG = MU.detectCloudProviders
end
require 'mu/master/ssl'
MU::Master::SSL.bootstrap
puts $MU_CFG.to_yaml
saveMuConfig($MU_CFG)
MU::MommaCat.restart
exit
end
if AMROOT and ($INITIALIZE or $CHANGES.include?("hostname"))
_system("/bin/hostname #{$MU_CFG['hostname']}")
end
def updateChefRbs
user = AMROOT ? "mu" : Etc.getpwuid(Process.uid).name
chefuser = user.gsub(/\./, "")
templates = { HOMEDIR+"/.chef/knife.rb" => KNIFE_TEMPLATE }
Dir.mkdir(HOMEDIR+"/.chef") if !Dir.exist?(HOMEDIR+"/.chef")
if AMROOT
templates["/etc/chef/client.rb"] = CLIENT_TEMPLATE
templates["/etc/opscode/pivotal.rb"] = PIVOTAL_TEMPLATE
end
templates.each_pair { |file, template|
erb = ERB.new(template)
processed = erb.result(binding)
tmpfile = file+".tmp."+Process.pid.to_s
File.open(tmpfile, File::CREAT|File::TRUNC|File::RDWR, 0644){ |f|
f.puts processed
}
if !File.size?(file) or File.read(tmpfile) != File.read(file)
File.rename(tmpfile, file)
MU.log "Updated #{file}", MU::NOTICE
$CHANGES << "chefcerts"
else
File.unlink(tmpfile)
end
}
end
# Do some more basic-but-Chef-dependent configuration *before* we meddle with
# the Chef Server configuration, which depends on some of this (SSL certs and
# local firewall ports).
if AMROOT and ($INITIALIZE or $CHANGES.include?("chefartifacts"))
MU.log "Purging and re-uploading all Chef artifacts", MU::NOTICE
%x{/sbin/service iptables stop} if $INITIALIZE
if File.exists?("#{CHEF_CTL}")
_system("#{CHEF_CTL} start")
end
output = %x{MU_INSTALLDIR=#{MU_BASE} MU_LIBDIR=#{MU_BASE}/lib MU_DATADIR=#{MU_BASE}/var #{MU_BASE}/lib/bin/mu-upload-chef-artifacts}
if $?.exitstatus != 0
puts output
MU.log "mu-upload-chef-artifacts failed, can't proceed", MU::ERR
%x{/sbin/service iptables start} if !$INITIALIZE
exit 1
end
%x{/sbin/service iptables start} if !$INITIALIZE
end
Dir.chdir(Dir.home)
if $INITIALIZE and AMROOT
MU.log "Force open key firewall holes", MU::NOTICE
_system("#{CHEF_CLIENT} -o 'recipe[mu-master::firewall-holes]'")
end
if AMROOT
MU.log "Checking internal SSL signing authority and certificates", MU::NOTICE
if !_system("#{CHEF_CLIENT} -o 'recipe[mu-master::ssl-certs]'") and $INITIALIZE
MU.log "Got bad exit code trying to run recipe[mu-master::ssl-certs]', aborting", MU::ERR
exit 1
end
if !File.size?("#{$MU_CFG['datadir']}/ssl/mommacat.crt")
MU.log "I just ran recipe[mu-master::ssl-certs]', but #{$MU_CFG['datadir']}/ssl/mommacat.crt} is still missing. Bailing.", MU::ERR
exit 1
end
end
if AMROOT
updateChefRbs if !$INITIALIZE
erb = ERB.new(File.read("#{MU_BASE}/lib/cookbooks/mu-master/templates/default/chef-server.rb.erb"))
updated_server_cfg = erb.result(binding)
cfgpath = "/etc/opscode/chef-server.rb"
tmpfile = "/etc/opscode/chef-server.rb.#{Process.pid}"
File.open(tmpfile, File::CREAT|File::TRUNC|File::RDWR, 0644){ |f|
f.puts updated_server_cfg
}
if $INITIALIZE or !File.size?(cfgpath) or File.read(tmpfile) != File.read(cfgpath)
File.rename(tmpfile, cfgpath)
# Opscode can't seem to get things right with their postgres socket
Dir.mkdir("/var/run/postgresql", 0755) if !Dir.exist?("/var/run/postgresql")
if File.exist?("/tmp/.s.PGSQL.5432") and !File.exist?("/var/run/postgresql/.s.PGSQL.5432")
File.symlink("/tmp/.s.PGSQL.5432", "/var/run/postgresql/.s.PGSQL.5432")
elsif !File.exist?("/tmp/.s.PGSQL.5432") and File.exist?("/var/run/postgresql/.s.PGSQL.5432")
File.symlink("/var/run/postgresql/.s.PGSQL.5432", "/tmp/.s.PGSQL.5432")
end
MU.log "Chef Server config was modified, reconfiguring...", MU::NOTICE, details: updated_server_cfg
# XXX Some undocumented port Chef needs only on startup is being blocked by
# iptables. Something rabbitmq-related. Dopey workaround.
%x{/sbin/service iptables stop}
_system("#{CHEF_CTL} stop")
MU.retrier(wait: 10, max: 6, loop_if: Proc.new { $?.exitstatus != 0 }, loop_msg: "Trying to get chef-server-ctl reconfigure to work") {
_system("#{CHEF_CTL} reconfigure")
}
_system("#{CHEF_CTL} start")
%x{/sbin/service iptables start} if !$INITIALIZE
updateChefRbs
$CHANGES << "chefcerts"
else
File.unlink(tmpfile)
updateChefRbs
end
else
updateChefRbs
end
if $IN_AWS and AMROOT# and $IN_GEM
_system("#{MU_BASE}/lib/bin/mu-aws-setup --dns --sg --logs --ephemeral")
# XXX --ip? Do we really care?
end
if $IN_GOOGLE and AMROOT
_system("#{MU_BASE}/lib/bin/mu-gcp-setup --sg --logs")
end
if $IN_AZURE and AMROOT
_system("#{MU_BASE}/lib/bin/mu-azure-setup --sg")
end
if $INITIALIZE or $CHANGES.include?("chefcerts")
_system("rm -f #{HOMEDIR}/.chef/trusted_certs/* ; knife ssl fetch -c #{HOMEDIR}/.chef/knife.rb")
if AMROOT
_system("rm -f /etc/chef/trusted_certs/* ; knife ssl fetch -c /etc/chef/client.rb")
end
end
# knife ssl fetch isn't bright enough to nab our intermediate certs, which
# ironically becomes a problem when we use one from the real world. Jam it
# into knife and chef-client's faces thusly:
if $MU_CFG['ssl'] and $MU_CFG['ssl']['chain'] and File.size?($MU_CFG['ssl']['chain'])
cert = File.basename($MU_CFG['ssl']['chain'])
FileUtils.cp($MU_CFG['ssl']['chain'], HOMEDIR+"/.chef/trusted_certs/#{cert}")
File.chmod(0600, HOMEDIR+"/.chef/trusted_certs/#{cert}")
if AMROOT
File.chmod(0644, $MU_CFG['ssl']['chain'])
FileUtils.cp($MU_CFG['ssl']['chain'], "/etc/chef/trusted_certs/#{cert}")
end
end
if $MU_CFG['repos'] and $MU_CFG['repos'].size > 0
$MU_CFG['repos'].each { |repo|
repo.match(/\/([^\/]+?)(\.git)?$/)
shortname = Regexp.last_match(1)
repodir = MU.dataDir + "/" + shortname
if !Dir.exist?(repodir)
MU.log "Cloning #{repo} into #{repodir}", MU::NOTICE
Dir.chdir(MU.dataDir)
_system("/usr/bin/git clone #{repo}")
$CHANGES << "chefartifacts"
end
}
end
if !AMROOT
exit
end
begin
if File.exists?("#{CHEF_CTL}")
_system("#{CHEF_CTL} start")
end
MU::Groomer::Chef.getSecret(vault: "secrets", item: "consul")
rescue OpenSSL::SSL::SSLError => e
if !$INITIALIZE
raise e
end
MU.log "Got SSL error connecting to Chef for vault secrets, this is normal during initial install", MU::NOTICE, details: e.message
rescue MU::Groomer::MuNoSuchSecret
data = {
"private_key" => File.read("#{MU_BASE}/var/ssl/consul.key"),
"certificate" => File.read("#{MU_BASE}/var/ssl/consul.crt"),
"ca_certificate" => File.read("#{MU_BASE}/var/ssl/Mu_CA.pem")
}
MU::Groomer::Chef.saveSecret(
vault: "secrets",
item: "consul",
data: data,
permissions: "name:MU-MASTER"
)
end
if $INITIALIZE or $CHANGES.include?("vault")
MU.log "Setting up Hashicorp Vault", MU::NOTICE
_system("#{CHEF_CLIENT} -o 'recipe[mu-master::vault]'")
end
set389DSCreds
if $MU_CFG['ldap']['type'] == "389 Directory Services"
begin
MU::Master::LDAP.listUsers
rescue Exception => e # XXX lazy exception handling is lazy
$CHANGES << "389ds"
end
if $INITIALIZE or $CHANGES.include?("389ds")
File.unlink("/root/389ds.tmp/389-directory-setup.inf") if File.exist?("/root/389ds.tmp/389-directory-setup.inf")
MU.log "Configuring 389 Directory Services", MU::NOTICE
_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
# Figure out if our run list is dumb
MU.log "Verifying MU-MASTER's Chef run list", MU::NOTICE
MU::Groomer::Chef.loadChefLib
chef_node = ::Chef::Node.load("MU-MASTER")
run_list = ["role[mu-master]"]
run_list.concat($MU_CFG['master_runlist_extras']) if $MU_CFG['master_runlist_extras'].is_a?(Array)
set_runlist = false
run_list.each { |rl|
set_runlist = true if !chef_node.run_list?(rl)
}
if set_runlist
MU.log "Updating MU-MASTER run_list", MU::NOTICE, details: run_list
chef_node.run_list(run_list)
chef_node.save
$CHANGES << "chefrun"
else
MU.log "Chef run list looks correct", MU::NOTICE, details: run_list
end
# TODO here are some things we don't do yet but should
# accommodate running as a non-root user
if $INITIALIZE
MU::Config.emitSchemaAsRuby
MU.log "Generating YARD documentation in /var/www/html/docs (see http://#{$MU_CFG['public_address']}/docs/frames.html)"
File.umask(0022)
_system("cd #{MU.myRoot} && umask 0022 && /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/")
end
MU.log "Running chef-client on MU-MASTER", MU::NOTICE
_system("#{CHEF_CLIENT} -o '#{run_list.join(",")}'")
if !File.exist?("#{MU_BASE}/var/users/mu/email") or !File.exist?("#{MU_BASE}/var/users/mu/realname")
MU.log "Finalizing the 'mu' Chef/LDAP account", MU::NOTICE
MU.setLogging(MU::Logger::SILENT)
MU::Master.manageUser(
"mu",
name: $MU_CFG['mu_admin_name'],
email: $MU_CFG['mu_admin_email'],
admin: true,
password: MU.generateWindowsPassword # we'll just overwrite this and do it with mu-user-manage below, which can do smart things with Scratchpad
)
MU.setLogging(MU::Logger::NORMAL)
sleep 3 # avoid LDAP lag for mu-user-manage
end
output = %x{/opt/chef/bin/knife vault show scratchpad 2>&1}
if $?.exitstatus != 0 or output.match(/is not a chef-vault/)
MU::Groomer::Chef.saveSecret(
vault: "scratchpad",
item: "placeholder",
data: { "secret" => "DO NOT DELETE", "timestamp" => "9999999999" },
permissions: "name:MU-MASTER"
)
end
MU.log "Regenerating documentation in /var/www/html/docs"
%x{#{CLEAN_ENV_STR} #{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
puts %x{#{CLEAN_ENV_STR} #{MU_BASE}/lib/bin/mu-user-manage -g mu -n "#{$MU_CFG['mu_admin_name']}"}
MU.log "If Scratchpad web interface is not accessible, try the following:", MU::NOTICE
puts "#{MU_BASE}/lib/bin/mu-user-manage -g --no-scratchpad mu".bold
end
if !ENV['PATH'].match(/(^|:)#{Regexp.quote(MU_BASE)}\/bin(:|$)/)
MU.log "I added some entries to your $PATH, run this to import them:", MU::NOTICE
puts "source #{HOMEDIR}/.bashrc".bold
end
end
if $IN_GEM
ansible_exec_path = MU::Groomer::Ansible.ansibleExecDir
if !ansible_exec_path or ansible_exec_path.empty?
puts "No Ansible executables found. Will not be able to groom servers!".red
end
end