diff options
Diffstat (limited to 'lib')
142 files changed, 3863 insertions, 4548 deletions
diff --git a/lib/facter/facter_dot_d.rb b/lib/facter/facter_dot_d.rb deleted file mode 100644 index b0584370..00000000 --- a/lib/facter/facter_dot_d.rb +++ /dev/null @@ -1,202 +0,0 @@ -# A Facter plugin that loads facts from /etc/facter/facts.d -# and /etc/puppetlabs/facter/facts.d. -# -# Facts can be in the form of JSON, YAML or Text files -# and any executable that returns key=value pairs. -# -# In the case of scripts you can also create a file that -# contains a cache TTL. For foo.sh store the ttl as just -# a number in foo.sh.ttl -# -# The cache is stored in /tmp/facts_cache.yaml as a mode -# 600 file and will have the end result of not calling your -# fact scripts more often than is needed - -class Facter::Util::DotD - require 'yaml' - - def initialize(dir="/etc/facts.d", cache_file=File.join(Puppet[:libdir], "facts_dot_d.cache")) - @dir = dir - @cache_file = cache_file - @cache = nil - @types = {".txt" => :txt, ".json" => :json, ".yaml" => :yaml} - end - - def entries - Dir.entries(@dir).reject { |f| f =~ /^\.|\.ttl$/ }.sort.map { |f| File.join(@dir, f) } - rescue - [] - end - - def fact_type(file) - extension = File.extname(file) - - type = @types[extension] || :unknown - - type = :script if type == :unknown && File.executable?(file) - - return type - end - - def txt_parser(file) - File.readlines(file).each do |line| - if line =~ /^([^=]+)=(.+)$/ - var = $1; val = $2 - - Facter.add(var) do - setcode { val } - end - end - end - rescue Exception => e - Facter.warn("Failed to handle #{file} as text facts: #{e.class}: #{e}") - end - - def json_parser(file) - begin - require 'json' - rescue LoadError - retry if require 'rubygems' - raise - end - - JSON.load(File.read(file)).each_pair do |f, v| - Facter.add(f) do - setcode { v } - end - end - rescue Exception => e - Facter.warn("Failed to handle #{file} as json facts: #{e.class}: #{e}") - end - - def yaml_parser(file) - require 'yaml' - - YAML.load_file(file).each_pair do |f, v| - Facter.add(f) do - setcode { v } - end - end - rescue Exception => e - Facter.warn("Failed to handle #{file} as yaml facts: #{e.class}: #{e}") - end - - def script_parser(file) - result = cache_lookup(file) - ttl = cache_time(file) - - unless result - result = Facter::Util::Resolution.exec(file) - - if ttl > 0 - Facter.debug("Updating cache for #{file}") - cache_store(file, result) - cache_save! - end - else - Facter.debug("Using cached data for #{file}") - end - - result.split("\n").each do |line| - if line =~ /^(.+)=(.+)$/ - var = $1; val = $2 - - Facter.add(var) do - setcode { val } - end - end - end - rescue Exception => e - Facter.warn("Failed to handle #{file} as script facts: #{e.class}: #{e}") - Facter.debug(e.backtrace.join("\n\t")) - end - - def cache_save! - cache = load_cache - File.open(@cache_file, "w", 0600) { |f| f.write(YAML.dump(cache)) } - rescue - end - - def cache_store(file, data) - load_cache - - @cache[file] = {:data => data, :stored => Time.now.to_i} - rescue - end - - def cache_lookup(file) - cache = load_cache - - return nil if cache.empty? - - ttl = cache_time(file) - - if cache[file] - now = Time.now.to_i - - return cache[file][:data] if ttl == -1 - return cache[file][:data] if (now - cache[file][:stored]) <= ttl - return nil - else - return nil - end - rescue - return nil - end - - def cache_time(file) - meta = file + ".ttl" - - return File.read(meta).chomp.to_i - rescue - return 0 - end - - def load_cache - unless @cache - if File.exist?(@cache_file) - @cache = YAML.load_file(@cache_file) - else - @cache = {} - end - end - - return @cache - rescue - @cache = {} - return @cache - end - - def create - entries.each do |fact| - type = fact_type(fact) - parser = "#{type}_parser" - - if respond_to?("#{type}_parser") - Facter.debug("Parsing #{fact} using #{parser}") - - send(parser, fact) - end - end - end -end - - -mdata = Facter.version.match(/(\d+)\.(\d+)\.(\d+)/) -if mdata - (major, minor, patch) = mdata.captures.map { |v| v.to_i } - if major < 2 - # Facter 1.7 introduced external facts support directly - unless major == 1 and minor > 6 - Facter::Util::DotD.new("/etc/facter/facts.d").create - Facter::Util::DotD.new("/etc/puppetlabs/facter/facts.d").create - - # Windows has a different configuration directory that defaults to a vendor - # specific sub directory of the %COMMON_APPDATA% directory. - if Dir.const_defined? 'COMMON_APPDATA' then - windows_facts_dot_d = File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'facter', 'facts.d') - Facter::Util::DotD.new(windows_facts_dot_d).create - end - end - end -end diff --git a/lib/facter/netmask_cidr_interface.rb b/lib/facter/netmask_cidr_interface.rb deleted file mode 100644 index d628d08c..00000000 --- a/lib/facter/netmask_cidr_interface.rb +++ /dev/null @@ -1,22 +0,0 @@ -# adds netmask facts for each interface in cidr notation -# i.e.: -# ... -# netmask_cidr_eth2 => 24 -# netmask_cidr_lo => 8 -# netmask_cidr_tun0 => 32 -# netmask_cidr_virbr0 => 24 -# ... - -require 'facter/util/ip' - -Facter::Util::IP.get_interfaces.each do |interface| - netmask = Facter.value("netmask_#{interface}") - if netmask != nil - Facter.add("netmask_cidr_" + interface ) do - setcode do - cidr_netmask=IPAddr.new(netmask).to_i.to_s(2).count("1") - cidr_netmask - end - end - end -end diff --git a/lib/facter/pe_version.rb b/lib/facter/pe_version.rb deleted file mode 100644 index 0cc0f64e..00000000 --- a/lib/facter/pe_version.rb +++ /dev/null @@ -1,53 +0,0 @@ -# Fact: is_pe, pe_version, pe_major_version, pe_minor_version, pe_patch_version -# -# Purpose: Return various facts about the PE state of the system -# -# Resolution: Uses a regex match against puppetversion to determine whether the -# machine has Puppet Enterprise installed, and what version (overall, major, -# minor, patch) is installed. -# -# Caveats: -# -Facter.add("pe_version") do - setcode do - pe_ver = Facter.value("puppetversion").match(/Puppet Enterprise (\d+\.\d+\.\d+)/) - pe_ver[1] if pe_ver - end -end - -Facter.add("is_pe") do - setcode do - if Facter.value(:pe_version).to_s.empty? then - false - else - true - end - end -end - -Facter.add("pe_major_version") do - confine :is_pe => true - setcode do - if pe_version = Facter.value(:pe_version) - pe_version.to_s.split('.')[0] - end - end -end - -Facter.add("pe_minor_version") do - confine :is_pe => true - setcode do - if pe_version = Facter.value(:pe_version) - pe_version.to_s.split('.')[1] - end - end -end - -Facter.add("pe_patch_version") do - confine :is_pe => true - setcode do - if pe_version = Facter.value(:pe_version) - pe_version.to_s.split('.')[2] - end - end -end diff --git a/lib/facter/puppet_vardir.rb b/lib/facter/puppet_vardir.rb deleted file mode 100644 index 0e6af40e..00000000 --- a/lib/facter/puppet_vardir.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This facter fact returns the value of the Puppet vardir setting for the node -# running puppet or puppet agent. The intent is to enable Puppet modules to -# automatically have insight into a place where they can place variable data, -# regardless of the node's platform. -# -# The value should be directly usable in a File resource path attribute. - - -begin - require 'facter/util/puppet_settings' -rescue LoadError => e - # puppet apply does not add module lib directories to the $LOAD_PATH (See - # #4248). It should (in the future) but for the time being we need to be - # defensive which is what this rescue block is doing. - rb_file = File.join(File.dirname(__FILE__), 'util', 'puppet_settings.rb') - load rb_file if File.exists?(rb_file) or raise e -end - -Facter.add(:puppet_vardir) do - setcode do - # This will be nil if Puppet is not available. - Facter::Util::PuppetSettings.with_puppet do - Puppet[:vardir] - end - end -end diff --git a/lib/facter/root_home.rb b/lib/facter/root_home.rb deleted file mode 100644 index b4f87ff2..00000000 --- a/lib/facter/root_home.rb +++ /dev/null @@ -1,32 +0,0 @@ -# A facter fact to determine the root home directory. -# This varies on PE supported platforms and may be -# reconfigured by the end user. - -module Facter::Util::RootHome - class << self - def get_root_home - root_ent = Facter::Util::Resolution.exec("getent passwd root") - # The home directory is the sixth element in the passwd entry - # If the platform doesn't have getent, root_ent will be nil and we should - # return it straight away. - root_ent && root_ent.split(":")[5] - end - end -end - -Facter.add(:root_home) do - setcode { Facter::Util::RootHome.get_root_home } -end - -Facter.add(:root_home) do - confine :kernel => :darwin - setcode do - str = Facter::Util::Resolution.exec("dscacheutil -q user -a name root") - hash = {} - str.split("\n").each do |pair| - key,value = pair.split(/:/) - hash[key] = value - end - hash['dir'].strip - end -end diff --git a/lib/facter/util/puppet_settings.rb b/lib/facter/util/puppet_settings.rb deleted file mode 100644 index 1ad94521..00000000 --- a/lib/facter/util/puppet_settings.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Facter - module Util - module PuppetSettings - # This method is intended to provide a convenient way to evaluate a - # Facter code block only if Puppet is loaded. This is to account for the - # situation where the fact happens to be in the load path, but Puppet is - # not loaded for whatever reason. Perhaps the user is simply running - # facter without the --puppet flag and they happen to be working in a lib - # directory of a module. - def self.with_puppet - begin - Module.const_get("Puppet") - rescue NameError - nil - else - yield - end - end - end - end -end diff --git a/lib/leap_cli/commands/README b/lib/leap_cli/commands/README new file mode 100644 index 00000000..bec78179 --- /dev/null +++ b/lib/leap_cli/commands/README @@ -0,0 +1,11 @@ +This directory contains ruby source files that define the available sub- +commands of the `leap` executable. + +For example, the command: + + leap compile + +Lives in lib/leap_cli/commands/init.rb + +These files use a DSL (called GLI) for defining command suites. +See https://github.com/davetron5000/gli for more information. diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb new file mode 100644 index 00000000..1b311eee --- /dev/null +++ b/lib/leap_cli/commands/ca.rb @@ -0,0 +1,541 @@ +autoload :OpenSSL, 'openssl' +autoload :CertificateAuthority, 'certificate_authority' +autoload :Date, 'date' +require 'digest/md5' + +module LeapCli; module Commands + + desc "Manage X.509 certificates" + command :cert do |cert| + + cert.desc 'Creates two Certificate Authorities (one for validating servers and one for validating clients).' + cert.long_desc 'See see what values are used in the generation of the certificates (like name and key size), run `leap inspect provider` and look for the "ca" property. To see the details of the created certs, run `leap inspect <file>`.' + cert.command :ca do |ca| + ca.action do |global_options,options,args| + assert_config! 'provider.ca.name' + generate_new_certificate_authority(:ca_key, :ca_cert, provider.ca.name) + generate_new_certificate_authority(:client_ca_key, :client_ca_cert, provider.ca.name + ' (client certificates only!)') + end + end + + cert.desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes, but only if needed.' + cert.long_desc 'This command will a generate new certificate for a node if some value in the node has changed ' + + 'that is included in the certificate (like hostname or IP address), or if the old certificate will be expiring soon. ' + + 'Sometimes, you might want to force the generation of a new certificate, ' + + 'such as in the cases where you have changed a CA parameter for server certificates, like bit size or digest hash. ' + + 'In this case, use --force. If <node-filter> is empty, this command will apply to all nodes.' + cert.arg_name 'FILTER' + cert.command :update do |update| + update.switch 'force', :desc => 'Always generate new certificates', :negatable => false + update.action do |global_options,options,args| + update_certificates(manager.filter!(args), options) + end + end + + cert.desc 'Creates a Diffie-Hellman parameter file, needed for forward secret OpenVPN ciphers.' # (needed for server-side of some TLS connections) + cert.command :dh do |dh| + dh.action do |global_options,options,args| + long_running do + if cmd_exists?('certtool') + log 0, 'Generating DH parameters (takes a long time)...' + output = assert_run!('certtool --generate-dh-params --sec-param high') + output.sub! /.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1' + output << "\n" + write_file!(:dh_params, output) + else + log 0, 'Generating DH parameters (takes a REALLY long time)...' + output = OpenSSL::PKey::DH.generate(3248).to_pem + write_file!(:dh_params, output) + end + end + end + end + + # + # hints: + # + # inspect CSR: + # openssl req -noout -text -in files/cert/x.csr + # + # generate CSR with openssl to see how it compares: + # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr + # + # validate a CSR: + # http://certlogik.com/decoder/ + # + # nice details about CSRs: + # http://www.redkestrel.co.uk/Articles/CSR.html + # + cert.desc "Creates a CSR for use in buying a commercial X.509 certificate." + cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. "+ + "The properties used for this CSR come from `provider.ca.server_certificates`, "+ + "but may be overridden here." + cert.command :csr do |csr| + csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.' + csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name." + csr.flag ['unit', 'OU'], :arg_name => 'UNIT', :desc => "Set OU in distinguished name." + csr.flag 'email', :arg_name => 'EMAIL', :desc => "Set emailAddress in distinguished name." + csr.flag ['locality', 'L'], :arg_name => 'LOCALITY', :desc => "Set L in distinguished name." + csr.flag ['state', 'ST'], :arg_name => 'STATE', :desc => "Set ST in distinguished name." + csr.flag ['country', 'C'], :arg_name => 'COUNTRY', :desc => "Set C in distinguished name." + csr.flag :bits, :arg_name => 'BITS', :desc => "Override default certificate bit length" + csr.flag :digest, :arg_name => 'DIGEST', :desc => "Override default signature digest" + csr.action do |global_options,options,args| + assert_config! 'provider.domain' + assert_config! 'provider.name' + assert_config! 'provider.default_language' + assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.digest' + domain = options[:domain] || provider.domain + + unless global_options[:force] + assert_files_missing! [:commercial_key, domain], [:commercial_csr, domain], + :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.' + end + + server_certificates = provider.ca.server_certificates + + # RSA key + keypair = CertificateAuthority::MemoryKeyMaterial.new + bit_size = (options[:bits] || server_certificates.bit_size).to_i + log :generating, "%s bit RSA key" % bit_size do + keypair.generate_key(bit_size) + write_file! [:commercial_key, domain], keypair.private_key.to_pem + end + + # CSR + dn = CertificateAuthority::DistinguishedName.new + dn.common_name = domain + dn.organization = options[:organization] || provider.name[provider.default_language] + dn.ou = options[:organizational_unit] # optional + dn.email_address = options[:email] # optional + dn.country = options[:country] || server_certificates['country'] # optional + dn.state = options[:state] || server_certificates['state'] # optional + dn.locality = options[:locality] || server_certificates['locality'] # optional + + digest = options[:digest] || server_certificates.digest + log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do + csr = create_csr(dn, keypair, digest) + request = csr.to_x509_csr + write_file! [:commercial_csr, domain], csr.to_pem + end + + # Sign using our own CA, for use in testing but hopefully not production. + # It is not that commerical CAs are so secure, it is just that signing your own certs is + # a total drag for the user because they must click through dire warnings. + #if options[:sign] + log :generating, "self-signed x509 server certificate for testing purposes" do + cert = csr.to_cert + cert.serial_number.number = cert_serial_number(domain) + cert.not_before = yesterday + cert.not_after = yesterday.advance(:years => 1) + cert.parent = ca_root + cert.sign! domain_test_signing_profile + write_file! [:commercial_cert, domain], cert.to_pem + log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}" + end + #end + + # FAKE CA + unless file_exists? :commercial_ca_cert + log :using, "generated CA in place of commercial CA for testing purposes" do + write_file! :commercial_ca_cert, read_file!(:ca_cert) + log "please also replace this file with the CA cert from the commercial authority you use." + end + end + end + end + end + + protected + + # + # will generate new certificates for the specified nodes, if needed. + # + def update_certificates(nodes, options={}) + assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them' + assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.digest' + assert_config! 'provider.ca.server_certificates.life_span' + assert_config! 'common.x509.use' + + nodes.each_node do |node| + warn_if_commercial_cert_will_soon_expire(node) + if !node.x509.use + remove_file!([:node_x509_key, node.name]) + remove_file!([:node_x509_cert, node.name]) + elsif options[:force] || cert_needs_updating?(node) + generate_cert_for_node(node) + end + end + end + + private + + def generate_new_certificate_authority(key_file, cert_file, common_name) + assert_files_missing! key_file, cert_file + assert_config! 'provider.ca.name' + assert_config! 'provider.ca.bit_size' + assert_config! 'provider.ca.life_span' + + root = CertificateAuthority::Certificate.new + + # set subject + root.subject.common_name = common_name + possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address'] + provider.ca.keys.each do |key| + if possible.include?(key) + root.subject.send(key + '=', provider.ca[key]) + end + end + + # set expiration + root.not_before = yesterday + root.not_after = yesterday_advance(provider.ca.life_span) + + # generate private key + root.serial_number.number = 1 + root.key_material.generate_key(provider.ca.bit_size) + + # sign self + root.signing_entity = true + root.parent = root + root.sign!(ca_root_signing_profile) + + # save + write_file!(key_file, root.key_material.private_key.to_pem) + write_file!(cert_file, root.to_pem) + end + + # + # returns true if the certs associated with +node+ need to be regenerated. + # + def cert_needs_updating?(node) + if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name]) + return true + else + cert = load_certificate_file([:node_x509_cert, node.name]) + if !created_by_authority?(cert, ca_root) + log :updating, "cert for node '#{node.name}' because it was signed by an old CA root cert." + return true + end + if cert.not_after < Time.now.advance(:months => 2) + log :updating, "cert for node '#{node.name}' because it will expire soon" + return true + end + if cert.subject.common_name != node.domain.full + log :updating, "cert for node '#{node.name}' because domain.full has changed (was #{cert.subject.common_name}, now #{node.domain.full})" + return true + end + cert.openssl_body.extensions.each do |ext| + if ext.oid == "subjectAltName" + ips = [] + dns_names = [] + ext.value.split(",").each do |value| + value.strip! + ips << $1 if value =~ /^IP Address:(.*)$/ + dns_names << $1 if value =~ /^DNS:(.*)$/ + end + dns_names.sort! + if ips.first != node.ip_address + log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})" + return true + elsif dns_names != dns_names_for_node(node) + log :updating, "cert for node '#{node.name}' because domain name aliases have changed\n from: #{dns_names.inspect}\n to: #{dns_names_for_node(node).inspect})" + return true + end + end + end + end + return false + end + + def created_by_authority?(cert, ca) + authority_key_id = cert.extensions["authorityKeyIdentifier"].identifier.sub(/^keyid:/, '') + authority_key_id == public_key_id_for_ca(ca) + end + + # calculate the "key id" for a root CA, that matches the value + # Authority Key Identifier in the x509 extensions of a cert. + def public_key_id_for_ca(ca_cert) + @ca_key_ids ||= {} + @ca_key_ids[ca_cert.object_id] ||= begin + pubkey = ca_cert.key_material.public_key + seq = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer.new(pubkey.n), + OpenSSL::ASN1::Integer.new(pubkey.e) + ]) + Digest::SHA1.hexdigest(seq.to_der).upcase.scan(/../).join(':') + end + end + + def warn_if_commercial_cert_will_soon_expire(node) + dns_names_for_node(node).each do |domain| + if file_exists?([:commercial_cert, domain]) + cert = load_certificate_file([:commercial_cert, domain]) + path = Path.relative_path([:commercial_cert, domain]) + if cert.not_after < Time.now.utc + log :error, "the commercial certificate '#{path}' has EXPIRED! " + + "You should renew it with `leap cert csr --domain #{domain}`." + elsif cert.not_after < Time.now.advance(:months => 2) + log :warning, "the commercial certificate '#{path}' will expire soon (#{cert.not_after}). "+ + "You should renew it with `leap cert csr --domain #{domain}`." + end + end + end + end + + def generate_cert_for_node(node) + return if node.x509.use == false + + cert = CertificateAuthority::Certificate.new + + # set subject + cert.subject.common_name = node.domain.full + cert.serial_number.number = cert_serial_number(node.domain.full) + + # set expiration + cert.not_before = yesterday + cert.not_after = yesterday_advance(provider.ca.server_certificates.life_span) + + # generate key + cert.key_material.generate_key(provider.ca.server_certificates.bit_size) + + # sign + cert.parent = ca_root + cert.sign!(server_signing_profile(node)) + + # save + write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem) + write_file!([:node_x509_cert, node.name], cert.to_pem) + end + + # + # yields client key and cert suitable for testing + # + def generate_test_client_cert(prefix=nil) + cert = CertificateAuthority::Certificate.new + cert.serial_number.number = cert_serial_number(provider.domain) + cert.subject.common_name = [prefix, random_common_name(provider.domain)].join + cert.not_before = yesterday + cert.not_after = yesterday.advance(:years => 1) + cert.key_material.generate_key(1024) # just for testing, remember! + cert.parent = client_ca_root + cert.sign! client_test_signing_profile + yield cert.key_material.private_key.to_pem, cert.to_pem + end + + # + # creates a CSR and returns it. + # with the correct extReq attribute so that the CA + # doens't generate certs with extensions we don't want. + # + def create_csr(dn, keypair, digest) + csr = CertificateAuthority::SigningRequest.new + csr.distinguished_name = dn + csr.key_material = keypair + csr.digest = digest + + # define extensions manually (library doesn't support setting these on CSRs) + extensions = [] + extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic| + basic.ca = false + } + extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage| + keyusage.usage = ["digitalSignature", "keyEncipherment"] + } + extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage| + extkeyusage.usage = [ "serverAuth"] + } + + # convert extensions to attribute 'extReq' + # aka "Requested Extensions" + factory = OpenSSL::X509::ExtensionFactory.new + attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence( + extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)} + )]) + attrs = [ + OpenSSL::X509::Attribute.new("extReq", attrval), + ] + csr.attributes = attrs + + return csr + end + + def ca_root + @ca_root ||= begin + load_certificate_file(:ca_cert, :ca_key) + end + end + + def client_ca_root + @client_ca_root ||= begin + load_certificate_file(:client_ca_cert, :client_ca_key) + end + end + + def load_certificate_file(crt_file, key_file=nil, password=nil) + crt = read_file!(crt_file) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + if key_file + key = read_file!(key_file) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password) + end + return cert + end + + def ca_root_signing_profile + { + "extensions" => { + "basicConstraints" => {"ca" => true}, + "keyUsage" => { + "usage" => ["critical", "keyCertSign"] + }, + "extendedKeyUsage" => { + "usage" => [] + } + } + } + end + + # + # For keyusage, openvpn server certs can have keyEncipherment or keyAgreement. + # Web browsers seem to break without keyEncipherment. + # For now, I am using digitalSignature + keyEncipherment + # + # * digitalSignature -- for (EC)DHE cipher suites + # "The digitalSignature bit is asserted when the subject public key is used + # with a digital signature mechanism to support security services other + # than certificate signing (bit 5), or CRL signing (bit 6). Digital + # signature mechanisms are often used for entity authentication and data + # origin authentication with integrity." + # + # * keyEncipherment ==> for plain RSA cipher suites + # "The keyEncipherment bit is asserted when the subject public key is used for + # key transport. For example, when an RSA key is to be used for key management, + # then this bit is set." + # + # * keyAgreement ==> for used with DH, not RSA. + # "The keyAgreement bit is asserted when the subject public key is used for key + # agreement. For example, when a Diffie-Hellman key is to be used for key + # management, then this bit is set." + # + # digest options: SHA512, SHA256, SHA1 + # + def server_signing_profile(node) + { + "digest" => provider.ca.server_certificates.digest, + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature", "keyEncipherment"] + }, + "extendedKeyUsage" => { + "usage" => ["serverAuth", "clientAuth"] + }, + "subjectAltName" => { + "ips" => [node.ip_address], + "dns_names" => dns_names_for_node(node) + } + } + } + end + + # + # This is used when signing the main cert for the provider's domain + # with our own CA (for testing purposes). Typically, this cert would + # be purchased from a commercial CA, and not signed this way. + # + def domain_test_signing_profile + { + "digest" => "SHA256", + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature", "keyEncipherment"] + }, + "extendedKeyUsage" => { + "usage" => ["serverAuth"] + } + } + } + end + + # + # This is used when signing a dummy client certificate that is only to be + # used for testing. + # + def client_test_signing_profile + { + "digest" => "SHA256", + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature"] + }, + "extendedKeyUsage" => { + "usage" => ["clientAuth"] + } + } + } + end + + def dns_names_for_node(node) + names = [node.domain.internal, node.domain.full] + if node['dns'] && node.dns['aliases'] && node.dns.aliases.any? + names += node.dns.aliases + end + names.compact! + names.sort! + names.uniq! + return names + end + + # + # For cert serial numbers, we need a non-colliding number less than 160 bits. + # md5 will do nicely, since there is no need for a secure hash, just a short one. + # (md5 is 128 bits) + # + def cert_serial_number(domain_name) + Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16) + end + + # + # for the random common name, we need a text string that will be unique across all certs. + # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid + # + def random_common_name(domain_name) + cert_serial_number(domain_name).to_s(36) + end + + # prints CertificateAuthority::DistinguishedName fields + def print_dn(dn) + fields = {} + [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr| + fields[attr] = dn.send(attr) if dn.send(attr) + end + fields.inspect + end + + ## + ## TIME HELPERS + ## + ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet + ## are behind UTC. + ## + + def yesterday + t = Time.now - 24*24*60 + Time.utc t.year, t.month, t.day + end + + def yesterday_advance(string) + number, unit = string.split(' ') + unless ['years', 'months', 'days', 'hours', 'minutes'].include? unit + bail!("The time property '#{string}' is missing a unit (one of: years, months, days, hours, minutes).") + end + unless number.to_i.to_s == number + bail!("The time property '#{string}' is missing a number.") + end + yesterday.advance(unit.to_sym => number.to_i) + end + +end; end diff --git a/lib/leap_cli/commands/clean.rb b/lib/leap_cli/commands/clean.rb new file mode 100644 index 00000000..a9afff53 --- /dev/null +++ b/lib/leap_cli/commands/clean.rb @@ -0,0 +1,16 @@ +module LeapCli + module Commands + + desc 'Removes all files generated with the "compile" command.' + command :clean do |c| + c.action do |global_options,options,args| + Dir.glob(path([:hiera, '*'])).each do |file| + remove_file! file + end + remove_file! path(:authorized_keys) + remove_file! path(:known_hosts) + end + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb new file mode 100644 index 00000000..f9079279 --- /dev/null +++ b/lib/leap_cli/commands/compile.rb @@ -0,0 +1,531 @@ +require 'socket' + +module LeapCli + module Commands + + desc "Compile generated files." + command [:compile, :c] do |c| + c.desc 'Compiles node configuration files into hiera files used for deployment.' + c.arg_name 'ENVIRONMENT', :optional => true + c.command :all do |all| + all.action do |global_options,options,args| + environment = args.first + compile_command(environment) + end + end + + c.desc "Prints a DNS zone file for your provider." + c.command :zone do |zone| + zone.action do |global_options, options, args| + compile_command(nil) + compile_zone_file(global_options[:yes] || global_options[:force]) + end + end + + c.desc "Print entries suitable for an /etc/hosts file, useful for testing your provider." + c.command :hosts do |hosts| + hosts.action do |global_options, options, args| + compile_command(nil) + compile_hosts_file + end + end + + c.desc "Compile provider.json bootstrap files for your provider." + c.command 'provider.json' do |provider| + provider.action do |global_options, options, args| + compile_command(nil) + compile_provider_json + end + end + + c.desc "Prints a list of firewall rules. These rules are already "+ + "implemented on each node, but you might want the list of all "+ + "rules in case you also have a restrictive network firewall." + c.command :firewall do |zone| + zone.action do |global_options, options, args| + compile_command(nil) + compile_firewall + end + end + + c.default_command :all + end + + protected + + def compile_command(environment) + if !LeapCli.leapfile.environment.nil? && !environment.nil? && environment != LeapCli.leapfile.environment + bail! "You cannot specify an ENVIRONMENT argument while the environment is pinned." + end + if environment + if manager.environment_names.include?(environment) + compile_hiera_files(manager.filter([environment]), false) + else + bail! "There is no environment named `#{environment}`." + end + else + clean_export = LeapCli.leapfile.environment.nil? + compile_hiera_files(manager.filter, clean_export) + end + if file_exists?(:static_web_readme) + compile_provider_json(environment) + end + end + + # + # a "clean" export of secrets will also remove keys that are no longer used, + # but this should not be done if we are not examining all possible nodes. + # + def compile_hiera_files(nodes, clean_export) + update_certificates(nodes) # \ must come first so that output will + update_compiled_ssh_configs # / get included in compiled hiera files. + sanity_check(nodes) + manager.export_nodes(nodes) + manager.export_secrets(clean_export) + end + + def update_compiled_ssh_configs + generate_monitor_ssh_keys + update_authorized_keys + update_known_hosts + end + + def sanity_check(nodes) + # confirm that every node has a unique ip address + ips = {} + nodes.pick_fields('ip_address').each do |name, ip_address| + if ips.key?(ip_address) + bail! { + log(:fatal_error, "Every node must have its own IP address.") { + log "Nodes `#{name}` and `#{ips[ip_address]}` are both configured with `#{ip_address}`." + } + } + else + ips[ip_address] = name + end + end + # confirm that the IP address of this machine is not also used for a node. + Socket.ip_address_list.each do |addrinfo| + if !addrinfo.ipv4_private? && ips.key?(addrinfo.ip_address) + ip = addrinfo.ip_address + name = ips[ip] + bail! { + log(:fatal_error, "Something is very wrong. The `leap` command must only be run on your sysadmin machine, not on a provider node.") { + log "This machine has the same IP address (#{ip}) as node `#{name}`." + } + } + end + end + end + + ## + ## SSH + ## + + # + # generates a ssh key pair that is used only by remote monitors + # to connect to nodes and run certain allowed commands. + # + # every node has the public monitor key added to their authorized + # keys, and every monitor node has a copy of the private monitor key. + # + def generate_monitor_ssh_keys + priv_key_file = path(:monitor_priv_key) + pub_key_file = path(:monitor_pub_key) + unless file_exists?(priv_key_file, pub_key_file) + ensure_dir(File.dirname(priv_key_file)) + ensure_dir(File.dirname(pub_key_file)) + cmd = %(ssh-keygen -N '' -C 'monitor' -t rsa -b 4096 -f '%s') % priv_key_file + assert_run! cmd + if file_exists?(priv_key_file, pub_key_file) + log :created, priv_key_file + log :created, pub_key_file + else + log :failed, 'to create monitor ssh keys' + end + end + end + + # + # Compiles the authorized keys file, which gets installed on every during init. + # Afterwards, puppet installs an authorized keys file that is generated differently + # (see authorized_keys() in macros.rb) + # + def update_authorized_keys + buffer = StringIO.new + keys = Dir.glob(path([:user_ssh, '*'])) + if keys.empty? + bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help add-user`." + end + if file_exists?(path(:monitor_pub_key)) + keys << path(:monitor_pub_key) + end + keys.sort.each do |keyfile| + ssh_type, ssh_key = File.read(keyfile).strip.split(" ") + buffer << ssh_type + buffer << " " + buffer << ssh_key + buffer << " " + buffer << Path.relative_path(keyfile) + buffer << "\n" + end + write_file!(:authorized_keys, buffer.string) + end + + # + # generates the known_hosts file. + # + # we do a 'late' binding on the hostnames and ip part of the ssh pub key record in order to allow + # for the possibility that the hostnames or ip has changed in the node configuration. + # + def update_known_hosts + buffer = StringIO.new + buffer << "#\n" + buffer << "# This file is automatically generated by the command `leap`. You should NOT modify this file.\n" + buffer << "# Instead, rerun `leap node init` on whatever node is causing SSH problems.\n" + buffer << "#\n" + manager.nodes.keys.sort.each do |node_name| + node = manager.nodes[node_name] + hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',') + pub_key = read_file([:node_ssh_pub_key,node.name]) + if pub_key + buffer << [hostnames, pub_key].join(' ') + buffer << "\n" + end + end + write_file!(:known_hosts, buffer.string) + end + + ## + ## provider.json + ## + + # + # generates static provider.json files that can put into place + # (e.g. https://domain/provider.json) for the cases where the + # webapp domain does not match the provider's domain. + # + def compile_provider_json(environments=nil) + webapp_nodes = manager.nodes[:services => 'webapp'] + write_file!(:static_web_readme, STATIC_WEB_README) + environments ||= manager.environment_names + environments.each do |env| + node = webapp_nodes[:environment => env].values.first + if node + env ||= 'default' + write_file!( + [:static_web_provider_json, env], + node['definition_files']['provider'] + ) + write_file!( + [:static_web_htaccess, env], + HTACCESS_FILE % {:min_version => manager.env(env).provider.client_version['min']} + ) + end + end + end + + HTACCESS_FILE = %[ +<Files provider.json> + Header set X-Minimum-Client-Version %{min_version} +</Files> +] + + STATIC_WEB_README = %[ +This directory contains statically rendered copies of the `provider.json` file +used by the client to "bootstrap" configure itself for use with your service +provider. + +There is a separate provider.json file for each environment, although you +should only need 'production/provider.json' or, if you have no environments +configured, 'default/provider.json'. + +To clarify, this is the public `provider.json` file used by the client, not the +`provider.json` file that is used to configure the provider. + +The provider.json file must be available at `https://domain/provider.json` +(unless this provider is included in the list of providers which are pre- +seeded in client). + +This provider.json file can be served correctly in one of three ways: + +(1) If the property webapp.domain is not configured, then the web app will be + installed at https://domain/ and it will handle serving the provider.json file. + +(2) If one or more nodes have the 'static' service configured for the provider's + domain, then these 'static' nodes will correctly serve provider.json. + +(3) Otherwise, you must copy the provider.json file to your web + server and make it available at '/provider.json'. The example htaccess + file shows what header options should be sent by the web server + with the response. + +This directory is needed for method (3), but not for methods (1) or (2). + +This directory has been created by the command `leap compile provider.json`. +Once created, it will be kept up to date everytime you compile. You may safely +remove this directory if you don't use it. +] + + ## + ## + ## ZONE FILE + ## + + def relative_hostname(fqdn, provider) + @domain_regexp ||= /\.?#{Regexp.escape(provider.domain)}$/ + fqdn.sub(@domain_regexp, '') + end + + # + # serial is any number less than 2^32 (4294967296) + # + def compile_zone_file(force=false) + # note: we use the default provider for all nodes, because we use it + # to generate hostnames that are relative to the default domain. + provider = manager.env('default').provider + hosts_seen = {} + lines = [] + + # + # header + # + lines << ZONE_HEADER % { + :domain => provider.domain, + :ns => provider.domain, + :contact => provider.contacts.default.first.sub('@','.'), + :serial => generate_zone_serial + } + + # + # common records + # + lines << ORIGIN_HEADER + # 'A' records for primary domain + manager.nodes[:environment => '!local'].each_node do |node| + if node.dns['aliases'] && node.dns.aliases.include?(provider.domain) + lines << ["@", "IN A #{node.ip_address}"] + end + end + + # NS records + if provider['dns'] && provider.dns['nameservers'] + unless provider.dns.nameservers.is_a?(Array) + # TODO: remove me once we have JSON schema working + bail! {log :error, 'dns.nameservers must be an array' } + end + provider.dns.nameservers.each do |ns| + lines << ["@", "IN NS #{ns}."] + end + elsif !force + log :warning, "Property dns.nameservers is not configured in provider.json." do + log "This will produce a zone file without any NS records." + log "Use --force to skip this warning." + end + return unless agree("Continue? ") + end + + # environment records + manager.environment_names.each do |env| + next if env == 'local' + nodes = manager.nodes[:environment => env] + next unless nodes.any? + spf = nil + dkim = nil + lines << ENV_HEADER % (env.nil? ? 'default' : env) + nodes.each_node do |node| + if node.dns.public + lines << [relative_hostname(node.domain.full, provider), "IN A #{node.ip_address}"] + end + if node.dns['aliases'] + node.dns.aliases.each do |host_alias| + if host_alias != node.domain.full && host_alias != provider.domain + lines << [relative_hostname(host_alias, provider), "IN A #{node.ip_address}"] + end + end + end + if node.services.include? 'mx' + mx_domain = relative_hostname(node.domain.full_suffix, provider) + lines << [mx_domain, "IN MX 10 #{relative_hostname(node.domain.full, provider)}"] + spf ||= [mx_domain, spf_record(node)] + dkim ||= dkim_record(node, provider) + end + end + lines << spf if spf + lines << dkim if dkim + end + + # print the lines + max_width = lines.inject(0) {|max, line| line.is_a?(Array) ? [max, line[0].length].max : max} + max_width = [max_width, 24].min + lines.each do |host, line| + if line.nil? + puts(host) + else + host = '@' if host == '' + puts("%-#{max_width}s %s" % [host, line]) + end + end + end + + # + # outputs entries suitable for an /etc/hosts file + # + def compile_hosts_file + manager.environment_names.each do |env| + nodes = manager.nodes[:environment => env] + next unless nodes.any? + puts + puts "## environment '#{env || 'default'}'" + nodes.each do |name, node| + puts "%s %s" % [ + node.ip_address, + [name, node.get('domain.full'), node.get('dns.aliases')].compact.join(' ') + ] + end + end + end + + private + + # + # allow mail from any mx node, plus the webapp nodes. + # + # TODO: ipv6 + # + def spf_record(node) + ips = node.nodes_like_me['services' => 'webapp'].values.collect {|n| + "ip4:" + n.ip_address + } + # TXT strings may not be longer than 255 characters, although + # you can chain multiple strings together. + strings = "v=spf1 MX #{ips.join(' ')} -all".scan(/.{1,255}/).join('" "') + %(IN TXT "#{strings}") + end + + # + # for example: + # + # selector._domainkey IN TXT "v=DKIM1;h=sha256;k=rsa;s=email;p=MIGfMA0GCSq...GSIb3DQ" + # + # specification: http://dkim.org/specs/rfc4871-dkimbase.html#rfc.section.7.4 + # + def dkim_record(node, provider) + # PEM encoded public key (base64), without the ---PUBLIC KEY--- armor parts. + assert_files_exist! :dkim_pub_key + dkim_pub_key = Path.named_path(:dkim_pub_key) + public_key = File.readlines(dkim_pub_key).grep(/^[^\-]+/).join + + host = relative_hostname( + node.mx.dkim.selector + "._domainkey." + node.domain.full_suffix, + provider) + + attrs = [ + "v=DKIM1", + "h=sha256", + "k=rsa", + "s=email", + "p=" + public_key + ] + + return [host, "IN TXT " + txt_wrap(attrs.join(';'))] + end + + # + # DNS TXT records cannot be longer than 255 characters. + # + # However, multiple responses will be concatenated together. + # It looks like this: + # + # IN TXT "v=spf1 .... first" "second string..." + # + def txt_wrap(str) + '"' + str.scan(/.{1,255}/).join('" "') + '"' + end + + # + # For zone serial number, we want something that will be + # different each time you deploy but also will be greater + # than any prior likely serial that was prefixed by the + # year, such as 2016040600. + # + # so, we use time_t of right now, modified with first + # digit incremented by one. + # + # this will work until Time.at(2**32 - 1_000_000_000) + # aka 2074-05-31 04:41:36 UTC. + # + def generate_zone_serial + Time.now.utc.to_i + 1_000_000_000 + end + + ENV_HEADER = %[ +;; +;; ENVIRONMENT %s +;; + +] + + ZONE_HEADER = %[ +;; +;; BIND data file for %{domain} +;; + +$TTL 600 +$ORIGIN %{domain}. + +@ IN SOA %{ns}. %{contact}. ( + %{serial} ; serial + 7200 ; refresh ( 24 hours) + 3600 ; retry ( 2 hours) + 1209600 ; expire (1000 hours) + 600 ) ; minimum ( 2 days) +; +] + + ORIGIN_HEADER = %[ +;; +;; ZONE ORIGIN +;; + +] + + ## + ## FIREWALL + ## + + public + + def compile_firewall + manager.nodes.each_node(&:evaluate) + + rules = [["ALLOW TO", "PORTS", "ALLOW FROM"]] + manager.nodes[:environment => '!local'].values.each do |node| + next unless node['firewall'] + node.firewall.each do |name, rule| + if rule.is_a? Hash + rules << add_rule(rule) + elsif rule.is_a? Array + rule.each do |r| + rules << add_rule(r) + end + end + end + end + + max_to = rules.inject(0) {|max, r| [max, r[0].length].max} + max_port = rules.inject(0) {|max, r| [max, r[1].length].max} + max_from = rules.inject(0) {|max, r| [max, r[2].length].max} + rules.each do |rule| + puts "%-#{max_to}s %-#{max_port}s %-#{max_from}s" % rule + end + end + + private + + def add_rule(rule) + [rule["to"], [rule["port"]].compact.join(','), rule["from"]] + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/db.rb b/lib/leap_cli/commands/db.rb new file mode 100644 index 00000000..5307ac4d --- /dev/null +++ b/lib/leap_cli/commands/db.rb @@ -0,0 +1,86 @@ +module LeapCli; module Commands + + desc 'Database commands.' + command :db do |db| + db.desc 'Destroy one or more databases. If present, limit to FILTER nodes. For example `leap db destroy --db sessions,tokens testing`.' + db.arg_name 'FILTER', :optional => true + db.command :destroy do |destroy| + destroy.flag :db, :arg_name => "DATABASES", :desc => 'Comma separated list of databases to destroy (no space). Use "--db all" to destroy all databases.', :optional => true + destroy.flag :user, :arg_name => "USERS", :desc => 'Comma separated list of usernames. The storage databases for these user(s) will be destroyed.', :optional => true + destroy.action do |global_options,options,args| + dbs = (options[:db]||"").split(',') + users = (options[:user]||"").split(',') + if dbs.empty? && users.empty? + bail!('Either --db or --user is required.') + end + nodes = manager.filter(args) + if nodes.any? + nodes = nodes[:services => 'couchdb'] + end + unless nodes.any? + bail! 'No db nodes selected.' + end + if users.any? + unless global_options[:yes] + say 'You are about to permanently destroy user databases for [%s] for nodes [%s].' % [users.join(', '), nodes.keys.join(', ')] + bail! unless agree("Continue? ") + end + destroy_user_dbs(nodes, users) + elsif dbs.any? + unless global_options[:yes] + if dbs.include?('all') + say 'You are about to permanently destroy all database data for nodes [%s].' % nodes.keys.join(', ') + else + say 'You are about to permanently destroy databases [%s] for nodes [%s].' % [dbs.join(', '), nodes.keys.join(', ')] + end + bail! unless agree("Continue? ") + end + if dbs.include?('all') + destroy_all_dbs(nodes) + else + destroy_dbs(nodes, dbs) + end + say 'You must run `leap deploy` in order to create the databases again.' + end + end + end + end + + private + + def destroy_all_dbs(nodes) + ssh_connect(nodes) do |ssh| + ssh.run('/etc/init.d/bigcouch stop && test ! -z "$(ls /opt/bigcouch/var/lib/ 2> /dev/null)" && rm -r /opt/bigcouch/var/lib/* && echo "All DBs destroyed" || echo "DBs already destroyed"') + end + end + + def destroy_dbs(nodes, dbs) + nodes.each_node do |node| + ssh_connect(node) do |ssh| + dbs.each do |db| + ssh.run(DESTROY_DB_COMMAND % {:db => db}) + end + end + end + end + + def destroy_user_dbs(nodes, users) + nodes.each_node do |node| + ssh_connect(node) do |ssh| + users.each do |user| + ssh.run(DESTROY_USER_DB_COMMAND % {:user => user}) + end + end + end + end + + DESTROY_DB_COMMAND = %{ +if [ 200 = `curl -ns -w "%%{http_code}" -X GET "127.0.0.1:5984/%{db}" -o /dev/null` ]; then + echo "Result from DELETE /%{db}:" `curl -ns -X DELETE "127.0.0.1:5984/%{db}"`; +else + echo "Skipping db '%{db}': it does not exist or has already been deleted."; +fi +} + + DESTROY_USER_DB_COMMAND = %{/srv/leap/couchdb/scripts/destroy-user-db --username %{user}} +end; end diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb new file mode 100644 index 00000000..9dd190ab --- /dev/null +++ b/lib/leap_cli/commands/deploy.rb @@ -0,0 +1,374 @@ +require 'etc' + +module LeapCli + module Commands + + desc 'Apply recipes to a node or set of nodes.' + long_desc 'The FILTER can be the name of a node, service, or tag.' + arg_name 'FILTER' + command [:deploy, :d] do |c| + + c.switch :fast, :desc => 'Makes the deploy command faster by skipping some slow steps. A "fast" deploy can be used safely if you recently completed a normal deploy.', + :negatable => false + + c.switch :sync, :desc => "Sync files, but don't actually apply recipes.", :negatable => false + + c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false + + c.switch :downgrade, :desc => 'Allows deploy to run with an older platform version.', :negatable => false + + c.switch :dev, :desc => "Development mode: don't run 'git submodule update' before deploy.", :negatable => false + + c.flag :tags, :desc => 'Specify tags to pass through to puppet (overriding the default).', + :arg_name => 'TAG[,TAG]' + + c.flag :port, :desc => 'Override the default SSH port.', + :arg_name => 'PORT' + + c.flag :ip, :desc => 'Override the default SSH IP address.', + :arg_name => 'IPADDRESS' + + c.action do |global,options,args| + + if options[:dev] != true + init_submodules + end + + nodes = manager.filter!(args, :disabled => false) + if nodes.size > 1 + say "Deploying to these nodes: #{nodes.keys.join(', ')}" + if !global[:yes] && !agree("Continue? ") + quit! "OK. Bye." + end + end + + environments = nodes.field('environment').uniq + if environments.empty? + environments = [nil] + end + environments.each do |env| + check_platform_pinning(env, global) + end + + # compile hiera files for all the nodes in every environment that is + # being deployed and only those environments. + compile_hiera_files(manager.filter(environments), false) + + ssh_connect(nodes, connect_options(options)) do |ssh| + ssh.leap.log :checking, 'node' do + ssh.leap.check_for_no_deploy + ssh.leap.assert_initialized + end + ssh.leap.log :synching, "configuration files" do + sync_hiera_config(ssh) + sync_support_files(ssh) + end + ssh.leap.log :synching, "puppet manifests" do + sync_puppet_files(ssh) + end + unless options[:sync] + ssh.leap.log :applying, "puppet" do + ssh.puppet.apply(:verbosity => [LeapCli.log_level,5].min, + :tags => tags(options), + :force => options[:force], + :info => deploy_info, + :downgrade => options[:downgrade] + ) + end + end + end + if !Util.exit_status.nil? && Util.exit_status != 0 + log :warning, "puppet did not finish successfully." + end + end + end + + desc 'Display recent deployment history for a set of nodes.' + long_desc 'The FILTER can be the name of a node, service, or tag.' + arg_name 'FILTER' + command [:history, :h] do |c| + c.flag :port, :desc => 'Override the default SSH port.', + :arg_name => 'PORT' + c.flag :ip, :desc => 'Override the default SSH IP address.', + :arg_name => 'IPADDRESS' + c.switch :last, :desc => 'Show last deploy only', + :negatable => false + c.action do |global,options,args| + if options[:last] == true + lines = 1 + else + lines = 10 + end + nodes = manager.filter!(args) + ssh_connect(nodes, connect_options(options)) do |ssh| + ssh.leap.history(lines) + end + end + end + + private + + def forcible_prompt(forced, msg, prompt) + say(msg) + if forced + log :warning, "continuing anyway because of --force" + else + say "hint: use --force to skip this prompt." + quit!("OK. Bye.") unless agree(prompt) + end + end + + # + # The currently activated provider.json could have loaded some pinning + # information for the platform. If this is the case, refuse to deploy + # if there is a mismatch. + # + # For example: + # + # "platform": { + # "branch": "develop" + # "version": "1.0..99" + # "commit": "e1d6280e0a8c565b7fb1a4ed3969ea6fea31a5e2..HEAD" + # } + # + def check_platform_pinning(environment, global_options) + provider = manager.env(environment).provider + return unless provider['platform'] + + if environment.nil? || environment == 'default' + provider_json = 'provider.json' + else + provider_json = 'provider.' + environment + '.json' + end + + # can we have json schema verification already? + unless provider.platform.is_a? Hash + bail!('`platform` attribute in #{provider_json} must be a hash (was %s).' % provider.platform.inspect) + end + + # check version + if provider.platform['version'] + if !Leap::Platform.version_in_range?(provider.platform.version) + forcible_prompt( + global_options[:force], + "The platform is pinned to a version range of '#{provider.platform.version}' "+ + "by the `platform.version` property in #{provider_json}, but the platform "+ + "(#{Path.platform}) has version #{Leap::Platform.version}.", + "Do you really want to deploy from the wrong version? " + ) + end + end + + # check branch + if provider.platform['branch'] + if !is_git_directory?(Path.platform) + forcible_prompt( + global_options[:force], + "The platform is pinned to a particular branch by the `platform.branch` property "+ + "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.", + "Do you really want to deploy anyway? " + ) + end + unless provider.platform.branch == current_git_branch(Path.platform) + forcible_prompt( + global_options[:force], + "The platform is pinned to branch '#{provider.platform.branch}' by the `platform.branch` property "+ + "in #{provider_json}, but the current branch is '#{current_git_branch(Path.platform)}' " + + "(for directory '#{Path.platform}')", + "Do you really want to deploy from the wrong branch? " + ) + end + end + + # check commit + if provider.platform['commit'] + if !is_git_directory?(Path.platform) + forcible_prompt( + global_options[:force], + "The platform is pinned to a particular commit range by the `platform.commit` property "+ + "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.", + "Do you really want to deploy anyway? " + ) + end + current_commit = current_git_commit(Path.platform) + Dir.chdir(Path.platform) do + commit_range = assert_run!("git log --pretty='format:%H' '#{provider.platform.commit}'", + "The platform is pinned to a particular commit range by the `platform.commit` property "+ + "in #{provider_json}, but git was not able to find commits in the range specified "+ + "(#{provider.platform.commit}).") + commit_range = commit_range.split("\n") + if !commit_range.include?(current_commit) && + provider.platform.commit.split('..').first != current_commit + forcible_prompt( + global_options[:force], + "The platform is pinned via the `platform.commit` property in #{provider_json} " + + "to a commit in the range #{provider.platform.commit}, but the current HEAD " + + "(#{current_commit}) is not in that range.", + "Do you really want to deploy from the wrong commit? " + ) + end + end + end + end + + def sync_hiera_config(ssh) + ssh.rsync.update do |server| + node = manager.node(server.host) + hiera_file = Path.relative_path([:hiera, node.name]) + ssh.leap.log hiera_file + ' -> ' + node.name + ':' + Leap::Platform.hiera_path + { + :source => hiera_file, + :dest => Leap::Platform.hiera_path, + :flags => "-rltp --chmod=u+rX,go-rwx" + } + end + end + + # + # sync various support files. + # + def sync_support_files(ssh) + dest_dir = Leap::Platform.files_dir + custom_files = build_custom_file_list + ssh.rsync.update do |server| + node = manager.node(server.host) + files_to_sync = node.file_paths.collect {|path| Path.relative_path(path, Path.provider) } + files_to_sync += custom_files + if files_to_sync.any? + ssh.leap.log(files_to_sync.join(', ') + ' -> ' + node.name + ':' + dest_dir) + { + :chdir => Path.named_path(:files_dir), + :source => ".", + :dest => dest_dir, + :excludes => "*", + :includes => calculate_includes_from_files(files_to_sync, '/files'), + :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --copy-links" + } + else + nil + end + end + end + + def sync_puppet_files(ssh) + ssh.rsync.update do |server| + ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + Leap::Platform.leap_dir) + { + :dest => Leap::Platform.leap_dir, + :source => '.', + :chdir => Path.platform, + :excludes => '*', + :includes => ['/bin', '/bin/**', '/puppet', '/puppet/**', '/tests', '/tests/**'], + :flags => "-rlt --relative --delete --copy-links" + } + end + end + + # + # ensure submodules are up to date, if the platform is a git + # repository. + # + def init_submodules + return unless is_git_directory?(Path.platform) + Dir.chdir Path.platform do + assert_run! "git submodule sync" + statuses = assert_run! "git submodule status" + statuses.strip.split("\n").each do |status_line| + if status_line =~ /^[\+-]/ + submodule = status_line.split(' ')[1] + log "Updating submodule #{submodule}" + assert_run! "git submodule update --init #{submodule}" + end + end + end + end + + # + # converts an array of file paths into an array + # suitable for --include of rsync + # + # if set, `prefix` is stripped off. + # + def calculate_includes_from_files(files, prefix=nil) + return nil unless files and files.any? + + # prepend '/' (kind of like ^ for rsync) + includes = files.collect {|file| file =~ /^\// ? file : '/' + file } + + # include all sub files of specified directories + includes.size.times do |i| + if includes[i] =~ /\/$/ + includes << includes[i] + '**' + end + end + + # include all parent directories (required because of --exclude '*') + includes.size.times do |i| + path = File.dirname(includes[i]) + while(path != '/') + includes << path unless includes.include?(path) + path = File.dirname(path) + end + end + + if prefix + includes.map! {|path| path.sub(/^#{Regexp.escape(prefix)}\//, '/')} + end + + return includes + end + + def tags(options) + if options[:tags] + tags = options[:tags].split(',') + else + tags = Leap::Platform.default_puppet_tags.dup + end + tags << 'leap_slow' unless options[:fast] + tags.join(',') + end + + # + # a provider might have various customization files that should be sync'ed to the server. + # this method builds that list of files to sync. + # + def build_custom_file_list + custom_files = [] + Leap::Platform.paths.keys.grep(/^custom_/).each do |path| + if file_exists?(path) + relative_path = Path.relative_path(path, Path.provider) + if dir_exists?(path) + custom_files << relative_path + '/' # rsync needs trailing slash + else + custom_files << relative_path + end + end + end + return custom_files + end + + def deploy_info + info = [] + info << "user: %s" % Etc.getpwuid(Process.euid).name + if is_git_directory?(Path.platform) && current_git_branch(Path.platform) != 'master' + info << "platform: %s (%s %s)" % [ + Leap::Platform.version, + current_git_branch(Path.platform), + current_git_commit(Path.platform)[0..4] + ] + else + info << "platform: %s" % Leap::Platform.version + end + if is_git_directory?(LEAP_CLI_BASE_DIR) + info << "leap_cli: %s (%s %s)" % [ + LeapCli::VERSION, + current_git_branch(LEAP_CLI_BASE_DIR), + current_git_commit(LEAP_CLI_BASE_DIR)[0..4] + ] + else + info << "leap_cli: %s" % LeapCli::VERSION + end + info.join(', ') + end + end +end diff --git a/lib/leap_cli/commands/env.rb b/lib/leap_cli/commands/env.rb new file mode 100644 index 00000000..80be2174 --- /dev/null +++ b/lib/leap_cli/commands/env.rb @@ -0,0 +1,76 @@ +module LeapCli + module Commands + + desc "Manipulate and query environment information." + long_desc "The 'environment' node property can be used to isolate sets of nodes into entirely separate environments. "+ + "A node in one environment will never interact with a node from another environment. "+ + "Environment pinning works by modifying your ~/.leaprc file and is dependent on the "+ + "absolute file path of your provider directory (pins don't apply if you move the directory)" + command [:env, :e] do |c| + c.desc "List the available environments. The pinned environment, if any, will be marked with '*'. Will also set the pin if run with an environment argument." + c.arg_name 'ENVIRONMENT', :optional => true + c.command :ls do |ls| + ls.action do |global_options, options, args| + environment = get_env_from_args(args) + if environment + pin(environment) + LeapCli.leapfile.load + end + print_envs + end + end + + c.desc 'Pin the environment to ENVIRONMENT. All subsequent commands will only apply to nodes in this environment.' + c.arg_name 'ENVIRONMENT' + c.command :pin do |pin| + pin.action do |global_options,options,args| + environment = get_env_from_args(args) + if environment + pin(environment) + else + bail! "There is no environment `#{environment}`" + end + end + end + + c.desc "Unpin the environment. All subsequent commands will apply to all nodes." + c.command :unpin do |unpin| + unpin.action do |global_options, options, args| + LeapCli.leapfile.unset('environment') + log 0, :saved, "~/.leaprc, removing environment property." + end + end + + c.default_command :ls + end + + protected + + def get_env_from_args(args) + environment = args.first + if environment == 'default' || (environment && manager.environment_names.include?(environment)) + return environment + else + return nil + end + end + + def pin(environment) + LeapCli.leapfile.set('environment', environment) + log 0, :saved, "~/.leaprc with environment set to #{environment}." + end + + def print_envs + envs = ["default"] + manager.environment_names.compact.sort + envs.each do |env| + if env + if LeapCli.leapfile.environment == env + puts "* #{env}" + else + puts " #{env}" + end + end + end + end + end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/facts.rb b/lib/leap_cli/commands/facts.rb new file mode 100644 index 00000000..11329ccc --- /dev/null +++ b/lib/leap_cli/commands/facts.rb @@ -0,0 +1,100 @@ +# +# Gather facter facts +# + +module LeapCli; module Commands + + desc 'Gather information on nodes.' + command :facts do |facts| + facts.desc 'Query servers to update facts.json.' + facts.long_desc "Queries every node included in FILTER and saves the important information to facts.json" + facts.arg_name 'FILTER' + facts.command :update do |update| + update.action do |global_options,options,args| + update_facts(global_options, options, args) + end + end + end + + protected + + def facter_cmd + 'facter --json ' + Leap::Platform.facts.join(' ') + end + + def remove_node_facts(name) + if file_exists?(:facts) + update_facts_file({name => nil}) + end + end + + def update_node_facts(name, facts) + update_facts_file({name => facts}) + end + + def rename_node_facts(old_name, new_name) + if file_exists?(:facts) + facts = JSON.parse(read_file(:facts) || {}) + facts[new_name] = facts[old_name] + facts[old_name] = nil + update_facts_file(facts, true) + end + end + + # + # if overwrite = true, then ignore existing facts.json. + # + def update_facts_file(new_facts, overwrite=false) + replace_file!(:facts) do |content| + if overwrite || content.nil? || content.empty? + old_facts = {} + else + old_facts = manager.facts + end + facts = old_facts.merge(new_facts) + facts.each do |name, value| + if value.is_a? String + if value == "" + value = nil + else + value = JSON.parse(value) rescue JSON::ParserError + end + end + if value.is_a? Hash + value.delete_if {|key,v| v.nil?} + end + facts[name] = value + end + facts.delete_if do |name, value| + value.nil? || value.empty? + end + if facts.empty? + "{}\n" + else + JSON.sorted_generate(facts) + "\n" + end + end + end + + private + + def update_facts(global_options, options, args) + nodes = manager.filter(args, :local => false, :disabled => false) + new_facts = {} + ssh_connect(nodes) do |ssh| + ssh.leap.run_with_progress(facter_cmd) do |response| + node = manager.node(response[:host]) + if node + new_facts[node.name] = response[:data].strip + else + log :warning, 'Could not find node for hostname %s' % response[:host] + end + end + end + # only overwrite the entire facts file if and only if we are gathering facts + # for all nodes in all environments. + overwrite_existing = args.empty? && LeapCli.leapfile.environment.nil? + update_facts_file(new_facts, overwrite_existing) + end + +end; end
\ No newline at end of file diff --git a/lib/leap_cli/commands/info.rb b/lib/leap_cli/commands/info.rb new file mode 100644 index 00000000..52225a94 --- /dev/null +++ b/lib/leap_cli/commands/info.rb @@ -0,0 +1,15 @@ +module LeapCli; module Commands + + desc 'Prints information regarding facts, history, and running processes for a node or nodes.' + long_desc 'The FILTER can be the name of a node, service, or tag.' + arg_name 'FILTER' + command [:info] do |c| + c.action do |global,options,args| + nodes = manager.filter!(args) + ssh_connect(nodes, connect_options(options)) do |ssh| + ssh.leap.debug + end + end + end + +end; end diff --git a/lib/leap_cli/commands/inspect.rb b/lib/leap_cli/commands/inspect.rb new file mode 100644 index 00000000..20654fa7 --- /dev/null +++ b/lib/leap_cli/commands/inspect.rb @@ -0,0 +1,144 @@ +module LeapCli; module Commands + + desc 'Prints details about a file. Alternately, the argument FILE can be the name of a node, service or tag.' + arg_name 'FILE' + command [:inspect, :i] do |c| + c.switch 'base', :desc => 'Inspect the FILE from the provider_base (i.e. without local inheritance).', :negatable => false + c.action do |global_options,options,args| + object = args.first + assert! object, 'A file path or node/service/tag name is required' + method = inspection_method(object) + if method && defined?(method) + self.send(method, object, options) + else + log "Sorry, I don't know how to inspect that." + end + end + end + + private + + FTYPE_MAP = { + "PEM certificate" => :inspect_x509_cert, + "PEM RSA private key" => :inspect_x509_key, + "OpenSSH RSA public key" => :inspect_ssh_pub_key, + "PEM certificate request" => :inspect_x509_csr + } + + def inspection_method(object) + if File.exists?(object) + ftype = `file #{object}`.split(':').last.strip + log 2, "file is of type '#{ftype}'" + if FTYPE_MAP[ftype] + FTYPE_MAP[ftype] + elsif File.extname(object) == ".json" + full_path = File.expand_path(object, Dir.pwd) + if path_match?(:node_config, full_path) + :inspect_node + elsif path_match?(:service_config, full_path) + :inspect_service + elsif path_match?(:tag_config, full_path) + :inspect_tag + elsif path_match?(:provider_config, full_path) || path_match?(:provider_env_config, full_path) + :inspect_provider + elsif path_match?(:common_config, full_path) + :inspect_common + else + nil + end + end + elsif manager.nodes[object] + :inspect_node + elsif manager.services[object] + :inspect_service + elsif manager.tags[object] + :inspect_tag + elsif object == "common" + :inspect_common + elsif object == "provider" + :inspect_provider + else + nil + end + end + + # + # inspectors + # + + def inspect_x509_key(file_path, options) + assert_bin! 'openssl' + puts assert_run! 'openssl rsa -in %s -text -check' % file_path + end + + def inspect_x509_cert(file_path, options) + assert_bin! 'openssl' + puts assert_run! 'openssl x509 -in %s -text -noout' % file_path + log 0, :"SHA256 fingerprint", X509.fingerprint("SHA256", file_path) + end + + def inspect_x509_csr(file_path, options) + assert_bin! 'openssl' + puts assert_run! 'openssl req -text -noout -verify -in %s' % file_path + end + + #def inspect_ssh_pub_key(file_path) + #end + + def inspect_node(arg, options) + inspect_json manager.nodes[name(arg)] + end + + def inspect_service(arg, options) + if options[:base] + inspect_json manager.base_services[name(arg)] + else + inspect_json manager.services[name(arg)] + end + end + + def inspect_tag(arg, options) + if options[:base] + inspect_json manager.base_tags[name(arg)] + else + inspect_json manager.tags[name(arg)] + end + end + + def inspect_provider(arg, options) + if options[:base] + inspect_json manager.base_provider + elsif arg =~ /provider\.(.*)\.json/ + inspect_json manager.env($1).provider + else + inspect_json manager.provider + end + end + + def inspect_common(arg, options) + if options[:base] + inspect_json manager.base_common + else + inspect_json manager.common + end + end + + # + # helpers + # + + def name(arg) + File.basename(arg).sub(/\.json$/, '') + end + + def inspect_json(config) + if config + puts JSON.sorted_generate(config) + end + end + + def path_match?(path_symbol, path) + Dir.glob(Path.named_path([path_symbol, '*'])).include?(path) + end + +end; end diff --git a/lib/leap_cli/commands/list.rb b/lib/leap_cli/commands/list.rb new file mode 100644 index 00000000..aa425432 --- /dev/null +++ b/lib/leap_cli/commands/list.rb @@ -0,0 +1,132 @@ +require 'command_line_reporter' + +module LeapCli; module Commands + + desc 'List nodes and their classifications' + long_desc 'Prints out a listing of nodes, services, or tags. ' + + 'If present, the FILTER can be a list of names of nodes, services, or tags. ' + + 'If the name is prefixed with +, this acts like an AND condition. ' + + "For example:\n\n" + + "`leap list node1 node2` matches all nodes named \"node1\" OR \"node2\"\n\n" + + "`leap list openvpn +local` matches all nodes with service \"openvpn\" AND tag \"local\"" + + arg_name 'FILTER', :optional => true + command [:list,:ls] do |c| + c.flag 'print', :desc => 'What attributes to print (optional)' + c.switch 'disabled', :desc => 'Include disabled nodes in the list.', :negatable => false + c.action do |global_options,options,args| + # don't rely on default manager(), because we want to pass custom options to load() + manager = LeapCli::Config::Manager.new + if global_options[:color] + colors = ['cyan', 'white'] + else + colors = [nil, nil] + end + puts + manager.load(:include_disabled => options['disabled'], :continue_on_error => true) + if options['print'] + print_node_properties(manager.filter(args), options['print']) + else + if args.any? + NodeTable.new(manager.filter(args), colors).run + else + environment = LeapCli.leapfile.environment || '_all_' + TagTable.new('SERVICES', manager.env(environment).services, colors).run + TagTable.new('TAGS', manager.env(environment).tags, colors).run + NodeTable.new(manager.filter(), colors).run + end + end + end + end + + private + + def self.print_node_properties(nodes, properties) + properties = properties.split(',') + max_width = nodes.keys.inject(0) {|max,i| [i.size,max].max} + nodes.each_node do |node| + value = properties.collect{|prop| + prop_value = node[prop] + if prop_value.nil? + "null" + elsif prop_value == "" + "empty" + elsif prop_value.is_a? LeapCli::Config::Object + node[prop].dump_json(:format => :compact) # TODO: add option of getting pre-evaluation values. + else + prop_value.to_s + end + }.join(', ') + printf("%#{max_width}s %s\n", node.name, value) + end + puts + end + + class TagTable + include CommandLineReporter + def initialize(heading, tag_list, colors) + @heading = heading + @tag_list = tag_list + @colors = colors + end + def run + tags = @tag_list.keys.select{|tag| tag !~ /^_/}.sort # sorted list of tags, excluding _partials + max_width = [20, (tags+[@heading]).inject(0) {|max,i| [i.size,max].max}].max + table :border => false do + row :color => @colors[0] do + column @heading, :align => 'right', :width => max_width + column "NODES", :width => HighLine::SystemExtensions.terminal_size.first - max_width - 2, :padding => 2 + end + tags.each do |tag| + next if @tag_list[tag].node_list.empty? + row :color => @colors[1] do + column tag + column @tag_list[tag].node_list.keys.sort.join(', ') + end + end + end + vertical_spacing + end + end + + # + # might be handy: HighLine::SystemExtensions.terminal_size.first + # + class NodeTable + include CommandLineReporter + def initialize(node_list, colors) + @node_list = node_list + @colors = colors + end + def run + rows = @node_list.keys.sort.collect do |node_name| + [node_name, @node_list[node_name].services.sort.join(', '), @node_list[node_name].tags.sort.join(', ')] + end + unless rows.any? + puts Paint["no results", :red] + puts + return + end + padding = 2 + max_node_width = [20, (rows.map{|i|i[0]} + ["NODES"] ).inject(0) {|max,i| [i.size,max].max}].max + max_service_width = (rows.map{|i|i[1]} + ["SERVICES"]).inject(0) {|max,i| [i.size+padding+padding,max].max} + max_tag_width = (rows.map{|i|i[2]} + ["TAGS"] ).inject(0) {|max,i| [i.size,max].max} + table :border => false do + row :color => @colors[0] do + column "NODES", :align => 'right', :width => max_node_width + column "SERVICES", :width => max_service_width, :padding => 2 + column "TAGS", :width => max_tag_width + end + rows.each do |r| + row :color => @colors[1] do + column r[0] + column r[1] + column r[2] + end + end + end + vertical_spacing + end + end + +end; end diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb new file mode 100644 index 00000000..a23661b3 --- /dev/null +++ b/lib/leap_cli/commands/node.rb @@ -0,0 +1,188 @@ +# +# fyi: the `node init` command lives in node_init.rb, +# but all other `node x` commands live here. +# + +autoload :IPAddr, 'ipaddr' + +module LeapCli; module Commands + + ## + ## COMMANDS + ## + + desc 'Node management' + command [:node, :n] do |node| + node.desc 'Create a new configuration file for a node named NAME.' + node.long_desc ["If specified, the optional argument SEED can be used to seed values in the node configuration file.", + "The format is property_name:value.", + "For example: `leap node add web1 ip_address:1.2.3.4 services:webapp`.", + "To set nested properties, property name can contain '.', like so: `leap node add web1 ssh.port:44`", + "Separeate multiple values for a single property with a comma, like so: `leap node add mynode services:webapp,dns`"].join("\n\n") + node.arg_name 'NAME [SEED]' # , :optional => false, :multiple => false + node.command :add do |add| + add.switch :local, :desc => 'Make a local testing node (by automatically assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false + add.action do |global_options,options,args| + # argument sanity checks + name = args.first + assert_valid_node_name!(name, options[:local]) + assert_files_missing! [:node_config, name] + + # create and seed new node + node = Config::Node.new(manager.env) + if options[:local] + node['ip_address'] = pick_next_vagrant_ip_address + end + seed_node_data_from_cmd_line(node, args[1..-1]) + seed_node_data_from_template(node) + validate_ip_address(node) + begin + node['name'] = name + json = node.dump_json(:exclude => ['name']) + write_file!([:node_config, name], json + "\n") + if file_exists? :ca_cert, :ca_key + generate_cert_for_node(manager.reload_node!(node)) + end + rescue LeapCli::ConfigError => exc + remove_node_files(name) + end + end + end + + node.desc 'Renames a node file, and all its related files.' + node.arg_name 'OLD_NAME NEW_NAME' + node.command :mv do |mv| + mv.action do |global_options,options,args| + node = get_node_from_args(args, include_disabled: true) + new_name = args.last + assert_valid_node_name!(new_name, node.vagrant?) + ensure_dir [:node_files_dir, new_name] + Leap::Platform.node_files.each do |path| + rename_file! [path, node.name], [path, new_name] + end + remove_directory! [:node_files_dir, node.name] + rename_node_facts(node.name, new_name) + end + end + + node.desc 'Removes all the files related to the node named NAME.' + node.arg_name 'NAME' #:optional => false #, :multiple => false + node.command :rm do |rm| + rm.action do |global_options,options,args| + node = get_node_from_args(args, include_disabled: true) + remove_node_files(node.name) + if node.vagrant? + vagrant_command("destroy --force", [node.name]) + end + remove_node_facts(node.name) + end + end + end + + ## + ## PUBLIC HELPERS + ## + + def get_node_from_args(args, options={}) + node_name = args.first + node = manager.node(node_name) + if node.nil? && options[:include_disabled] + node = manager.disabled_node(node_name) + end + assert!(node, "Node '#{node_name}' not found.") + node + end + + def seed_node_data_from_cmd_line(node, args) + args.each do |seed| + key, value = seed.split(':', 2) + value = format_seed_value(value) + assert! key =~ /^[0-9a-z\._]+$/, "illegal characters used in property '#{key}'" + if key =~ /\./ + key_parts = key.split('.') + final_key = key_parts.pop + current_object = node + key_parts.each do |key_part| + current_object[key_part] ||= Config::Object.new + current_object = current_object[key_part] + end + current_object[final_key] = value + else + node[key] = value + end + end + end + + # + # load "new node template" information into the `node`, modifying `node`. + # values in the template will not override existing node values. + # + def seed_node_data_from_template(node) + node.inherit_from!(manager.template('common')) + [node['services']].flatten.each do |service| + if service + template = manager.template(service) + if template + node.inherit_from!(template) + end + end + end + end + + def remove_node_files(node_name) + (Leap::Platform.node_files + [:node_files_dir]).each do |path| + remove_file! [path, node_name] + end + end + + # + # conversions: + # + # "x,y,z" => ["x","y","z"] + # + # "22" => 22 + # + # "5.1" => 5.1 + # + def format_seed_value(v) + if v =~ /,/ + v = v.split(',') + v.map! do |i| + i = i.to_i if i.to_i.to_s == i + i = i.to_f if i.to_f.to_s == i + i + end + else + v = v.to_i if v.to_i.to_s == v + v = v.to_f if v.to_f.to_s == v + end + return v + end + + def validate_ip_address(node) + if node['ip_address'] == "REQUIRED" + bail! do + log :error, "ip_address is not set. Specify with `leap node add NAME ip_address:ADDRESS`." + end + end + IPAddr.new(node['ip_address']) + rescue ArgumentError + bail! do + if node['ip_address'] + log :invalid, "ip_address #{node['ip_address'].inspect}" + else + log :missing, "ip_address" + end + end + end + + def assert_valid_node_name!(name, local=false) + assert! name, 'No <node-name> specified.' + if local + assert! name =~ /^[0-9a-z]+$/, "illegal characters used in node name '#{name}' (note: Vagrant does not allow hyphens or underscores)" + else + assert! name =~ /^[0-9a-z-]+$/, "illegal characters used in node name '#{name}' (note: Linux does not allow underscores)" + end + end + +end; end diff --git a/lib/leap_cli/commands/node_init.rb b/lib/leap_cli/commands/node_init.rb new file mode 100644 index 00000000..33f6288d --- /dev/null +++ b/lib/leap_cli/commands/node_init.rb @@ -0,0 +1,169 @@ +# +# Node initialization. +# Most of the fun stuff is in tasks.rb. +# + +module LeapCli; module Commands + + desc 'Node management' + command :node do |node| + node.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages' + node.long_desc "This command prepares a server to be used with the LEAP Platform by saving the server's SSH host key, " + + "copying the authorized_keys file, installing packages that are required for deploying, and registering important facts. " + + "Node init must be run before deploying to a server, and the server must be running and available via the network. " + + "This command only needs to be run once, but there is no harm in running it multiple times." + node.arg_name 'FILTER' + node.command :init do |init| + init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false + init.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT' + init.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS' + + init.action do |global,options,args| + assert! args.any?, 'You must specify a FILTER' + finished = [] + manager.filter!(args).each_node do |node| + is_node_alive(node, options) + save_public_host_key(node, global, options) unless node.vagrant? + update_compiled_ssh_configs + ssh_connect_options = connect_options(options).merge({:bootstrap => true, :echo => options[:echo]}) + ssh_connect(node, ssh_connect_options) do |ssh| + if node.vagrant? + ssh.install_insecure_vagrant_key + end + ssh.install_authorized_keys + ssh.install_prerequisites + unless node.vagrant? + ssh.leap.log(:checking, "SSH host keys") do + ssh.leap.capture(get_ssh_keys_cmd) do |response| + update_local_ssh_host_keys(node, response[:data]) if response[:exitcode] == 0 + end + end + end + ssh.leap.log(:updating, "facts") do + ssh.leap.capture(facter_cmd) do |response| + if response[:exitcode] == 0 + update_node_facts(node.name, response[:data]) + else + log :failed, "to run facter on #{node.name}" + end + end + end + end + finished << node.name + end + log :completed, "initialization of nodes #{finished.join(', ')}" + end + end + end + + private + + ## + ## PRIVATE HELPERS + ## + + def is_node_alive(node, options) + address = options[:ip] || node.ip_address + port = options[:port] || node.ssh.port + log :connecting, "to node #{node.name}" + assert_run! "nc -zw3 #{address} #{port}", + "Failed to reach #{node.name} (address #{address}, port #{port}). You can override the configured IP address and port with --ip or --port." + end + + # + # saves the public ssh host key for node into the provider directory. + # + # see `man sshd` for the format of known_hosts + # + def save_public_host_key(node, global, options) + log :fetching, "public SSH host key for #{node.name}" + address = options[:ip] || node.ip_address + port = options[:port] || node.ssh.port + host_keys = get_public_keys_for_ip(address, port) + pub_key_path = Path.named_path([:node_ssh_pub_key, node.name]) + + if Path.exists?(pub_key_path) + if host_keys.include? SshKey.load(pub_key_path) + log :trusted, "- Public SSH host key for #{node.name} matches previously saved key", :indent => 1 + else + bail! do + log :error, "The public SSH host keys we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1 + log "Delete the file #{pub_key_path} if you really want to remove the trusted SSH host key.", :indent => 2 + end + end + else + known_key = host_keys.detect{|k|k.in_known_hosts?(node.name, node.ip_address, node.domain.name)} + if known_key + log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)" + else + public_key = SshKey.pick_best_key(host_keys) + if public_key.nil? + bail!("We got back #{host_keys.size} host keys from #{node.name}, but we can't support any of them.") + else + say(" This is the SSH host key you got back from node \"#{node.name}\"") + say(" Type -- #{public_key.bits} bit #{public_key.type.upcase}") + say(" Fingerprint -- " + public_key.fingerprint) + say(" Public Key -- " + public_key.key) + if !global[:yes] && !agree(" Is this correct? ") + bail! + else + known_key = public_key + end + end + end + puts + write_file! [:node_ssh_pub_key, node.name], known_key.to_s + end + end + + # + # Get the public host keys for a host using ssh-keyscan. + # Return an array of SshKey objects, one for each key. + # + def get_public_keys_for_ip(address, port=22) + assert_bin!('ssh-keyscan') + output = assert_run! "ssh-keyscan -p #{port} #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?" + if output.empty? + bail! :failed, "ssh-keyscan returned empty output." + end + + if output =~ /No route to host/ + bail! :failed, 'ssh-keyscan: no route to %s' % address + else + keys = SshKey.parse_keys(output) + if keys.empty? + bail! "ssh-keyscan got zero host keys back (that we understand)! Output was: #{output}" + else + return keys + end + end + end + + # run on the server to generate a string suitable for passing to SshKey.parse_keys() + def get_ssh_keys_cmd + "/bin/grep ^HostKey /etc/ssh/sshd_config | /usr/bin/awk '{print $2 \".pub\"}' | /usr/bin/xargs /bin/cat" + end + + # + # Sometimes the ssh host keys on the server will be better than what we have + # stored locally. In these cases, ask the user if they want to upgrade. + # + def update_local_ssh_host_keys(node, remote_keys_string) + remote_keys = SshKey.parse_keys(remote_keys_string) + return unless remote_keys.any? + current_key = SshKey.load(Path.named_path([:node_ssh_pub_key, node.name])) + best_key = SshKey.pick_best_key(remote_keys) + return unless best_key && current_key + if current_key != best_key + say(" One of the SSH host keys for node '#{node.name}' is better than what you currently have trusted.") + say(" Current key: #{current_key.summary}") + say(" Better key: #{best_key.summary}") + if agree(" Do you want to use the better key? ") + write_file! [:node_ssh_pub_key, node.name], best_key.to_s + end + else + log(3, "current host key does not need updating") + end + end + +end; end diff --git a/lib/leap_cli/commands/ssh.rb b/lib/leap_cli/commands/ssh.rb new file mode 100644 index 00000000..3887618e --- /dev/null +++ b/lib/leap_cli/commands/ssh.rb @@ -0,0 +1,225 @@ +module LeapCli; module Commands + + desc 'Log in to the specified node with an interactive shell.' + arg_name 'NAME' #, :optional => false, :multiple => false + command :ssh do |c| + c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. `--ssh '-F ~/sshconfig'`)." + c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.' + c.action do |global_options,options,args| + exec_ssh(:ssh, options, args) + end + end + + desc 'Log in to the specified node with an interactive shell using mosh (requires node to have mosh.enabled set to true).' + arg_name 'NAME' + command :mosh do |c| + c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. `--ssh '-F ~/sshconfig'`)." + c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.' + c.action do |global_options,options,args| + exec_ssh(:mosh, options, args) + end + end + + desc 'Creates an SSH port forward (tunnel) to the node NAME. REMOTE_PORT is the port on the remote node that the tunnel will connect to. LOCAL_PORT is the optional port on your local machine. For example: `leap tunnel couch1:5984`.' + arg_name '[LOCAL_PORT:]NAME:REMOTE_PORT' + command :tunnel do |c| + c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. --ssh '-F ~/sshconfig')." + c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.' + c.action do |global_options,options,args| + local_port, node, remote_port = parse_tunnel_arg(args.first) + unless node.ssh.config.AllowTcpForwarding == "yes" + log :warning, "It looks like TCP forwarding is not enabled. "+ + "The tunnel command requires that the node property ssh.config.AllowTcpForwarding "+ + "be set to 'yes'. Add this property to #{node.name}.json, deploy, and then try tunnel again." + end + options[:ssh] = [options[:ssh], "-N -L 127.0.0.1:#{local_port}:0.0.0.0:#{remote_port}"].join(' ') + log("Forward port localhost:#{local_port} to #{node.name}:#{remote_port}") + if is_port_available?(local_port) + exec_ssh(:ssh, options, [node.name]) + end + end + end + + desc 'Secure copy from FILE1 to FILE2. Files are specified as NODE_NAME:FILE_PATH. For local paths, omit "NODE_NAME:".' + arg_name 'FILE1 FILE2' + command :scp do |c| + c.switch :r, :desc => 'Copy recursively' + c.action do |global_options, options, args| + if args.size != 2 + bail!('You must specificy both FILE1 and FILE2') + end + from, to = args + if (from !~ /:/ && to !~ /:/) || (from =~ /:/ && to =~ /:/) + bail!('One FILE must be remote and the other local.') + end + src_node_name = src_file_path = src_node = nil + dst_node_name = dst_file_path = dst_node = nil + if from =~ /:/ + src_node_name, src_file_path = from.split(':') + src_node = get_node_from_args([src_node_name], :include_disabled => true) + dst_file_path = to + else + dst_node_name, dst_file_path = to.split(':') + dst_node = get_node_from_args([dst_node_name], :include_disabled => true) + src_file_path = from + end + exec_scp(options, src_node, src_file_path, dst_node, dst_file_path) + end + end + + protected + + # + # allow for ssh overrides of all commands that use ssh_connect + # + def connect_options(options) + connect_options = {:ssh_options=>{}} + if options[:port] + connect_options[:ssh_options][:port] = options[:port] + end + if options[:ip] + connect_options[:ssh_options][:host_name] = options[:ip] + end + return connect_options + end + + def ssh_config_help_message + puts "" + puts "Are 'too many authentication failures' getting you down?" + puts "Then we have the solution for you! Add something like this to your ~/.ssh/config file:" + puts " Host *.#{manager.provider.domain}" + puts " IdentityFile ~/.ssh/id_rsa" + puts " IdentitiesOnly=yes" + puts "(replace `id_rsa` with the actual private key filename that you use for this provider)" + end + + require 'socket' + def is_port_available?(port) + TCPServer.open('127.0.0.1', port) {} + true + rescue Errno::EACCES + bail!("You don't have permission to bind to port #{port}.") + rescue Errno::EADDRINUSE + bail!("Local port #{port} is already in use. Specify LOCAL_PORT to pick another.") + rescue Exception => exc + bail!(exc.to_s) + end + + private + + def exec_ssh(cmd, cli_options, args) + node = get_node_from_args(args, :include_disabled => true) + port = node.ssh.port + options = ssh_config(node) + username = 'root' + if LeapCli.log_level >= 3 + options << "-vv" + elsif LeapCli.log_level >= 2 + options << "-v" + end + if cli_options[:port] + port = cli_options[:port] + end + if cli_options[:ssh] + options << cli_options[:ssh] + end + ssh = "ssh -l #{username} -p #{port} #{options.join(' ')}" + if cmd == :ssh + command = "#{ssh} #{node.domain.full}" + elsif cmd == :mosh + command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.domain.full}" + end + log 2, command + + # exec the shell command in a subprocess + pid = fork { exec "#{command}" } + + Signal.trap("SIGINT") do + Process.kill("KILL", pid) + Process.wait(pid) + exit(0) + end + + # wait for shell to exit so we can grab the exit status + _, status = Process.waitpid2(pid) + + if status.exitstatus == 255 + ssh_config_help_message + elsif status.exitstatus != 0 + exit(status.exitstatus) + end + end + + def exec_scp(cli_options, src_node, src_file_path, dst_node, dst_file_path) + node = src_node || dst_node + options = ssh_config(node) + port = node.ssh.port + username = 'root' + options << "-r" if cli_options[:r] + scp = "scp -P #{port} #{options.join(' ')}" + if src_node + command = "#{scp} #{username}@#{src_node.domain.full}:#{src_file_path} #{dst_file_path}" + elsif dst_node + command = "#{scp} #{src_file_path} #{username}@#{dst_node.domain.full}:#{dst_file_path}" + end + log 2, command + + # exec the shell command in a subprocess + pid = fork { exec "#{command}" } + + Signal.trap("SIGINT") do + Process.kill("KILL", pid) + Process.wait(pid) + exit(0) + end + + # wait for shell to exit so we can grab the exit status + _, status = Process.waitpid2(pid) + exit(status.exitstatus) + end + + # + # SSH command line -o options. See `man ssh_config` + # + # NOTES: + # + # The option 'HostKeyAlias=#{node.name}' is oddly incompatible with ports in + # known_hosts file, so we must not use this or non-standard ports break. + # + def ssh_config(node) + options = [ + "-o 'HostName=#{node.ip_address}'", + "-o 'GlobalKnownHostsFile=#{path(:known_hosts)}'", + "-o 'UserKnownHostsFile=/dev/null'" + ] + if node.vagrant? + options << "-i #{vagrant_ssh_key_file}" # use the universal vagrant insecure key + options << "-o IdentitiesOnly=yes" # force the use of the insecure vagrant key + options << "-o 'StrictHostKeyChecking=no'" # blindly accept host key and don't save it + # (since userknownhostsfile is /dev/null) + else + options << "-o 'StrictHostKeyChecking=yes'" + end + if !node.supported_ssh_host_key_algorithms.empty? + options << "-o 'HostKeyAlgorithms=#{node.supported_ssh_host_key_algorithms}'" + end + return options + end + + def parse_tunnel_arg(arg) + if arg.count(':') == 1 + node_name, remote = arg.split(':') + local = nil + elsif arg.count(':') == 2 + local, node_name, remote = arg.split(':') + else + bail!('Argument NAME:REMOTE_PORT required.') + end + node = get_node_from_args([node_name], :include_disabled => true) + remote = remote.to_i + local = local || remote + local = local.to_i + return [local, node, remote] + end + +end; end
\ No newline at end of file diff --git a/lib/leap_cli/commands/test.rb b/lib/leap_cli/commands/test.rb new file mode 100644 index 00000000..73207b31 --- /dev/null +++ b/lib/leap_cli/commands/test.rb @@ -0,0 +1,74 @@ +module LeapCli; module Commands + + desc 'Run tests.' + command [:test, :t] do |test| + test.desc 'Run the test suit on FILTER nodes.' + test.arg_name 'FILTER', :optional => true + test.command :run do |run| + run.switch 'continue', :desc => 'Continue over errors and failures (default is --no-continue).', :negatable => true + run.action do |global_options,options,args| + test_order = File.join(Path.platform, 'tests/order.rb') + if File.exists?(test_order) + require test_order + end + manager.filter!(args).names_in_test_dependency_order.each do |node_name| + node = manager.nodes[node_name] + begin + ssh_connect(node) do |ssh| + ssh.run(test_cmd(options)) + end + rescue Capistrano::CommandError => exc + if options[:continue] + exit_status(1) + else + bail! + end + end + end + end + end + + test.desc 'Creates files needed to run tests.' + test.command :init do |init| + init.action do |global_options,options,args| + generate_test_client_openvpn_configs + end + end + + test.default_command :run + end + + private + + def test_cmd(options) + if options[:continue] + "#{Leap::Platform.leap_dir}/bin/run_tests --continue" + else + "#{Leap::Platform.leap_dir}/bin/run_tests" + end + end + + # + # generates a whole bunch of openvpn configs that can be used to connect to different openvpn gateways + # + def generate_test_client_openvpn_configs + assert_config! 'provider.ca.client_certificates.unlimited_prefix' + assert_config! 'provider.ca.client_certificates.limited_prefix' + template = read_file! Path.find_file(:test_client_openvpn_template) + manager.environment_names.each do |env| + vpn_nodes = manager.nodes[:environment => env][:services => 'openvpn']['openvpn.allow_limited' => true] + if vpn_nodes.any? + generate_test_client_cert(provider.ca.client_certificates.limited_prefix) do |key, cert| + write_file! [:test_openvpn_config, [env, 'limited'].compact.join('_')], Util.erb_eval(template, binding) + end + end + vpn_nodes = manager.nodes[:environment => env][:services => 'openvpn']['openvpn.allow_unlimited' => true] + if vpn_nodes.any? + generate_test_client_cert(provider.ca.client_certificates.unlimited_prefix) do |key, cert| + write_file! [:test_openvpn_config, [env, 'unlimited'].compact.join('_')], Util.erb_eval(template, binding) + end + end + end + end + +end; end diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb new file mode 100644 index 00000000..b842e854 --- /dev/null +++ b/lib/leap_cli/commands/user.rb @@ -0,0 +1,136 @@ + +# +# perhaps we want to verify that the key files are actually the key files we expect. +# we could use 'file' for this: +# +# > file ~/.gnupg/00440025.asc +# ~/.gnupg/00440025.asc: PGP public key block +# +# > file ~/.ssh/id_rsa.pub +# ~/.ssh/id_rsa.pub: OpenSSH RSA public key +# + +module LeapCli + module Commands + + desc 'Adds a new trusted sysadmin by adding public keys to the "users" directory.' + arg_name 'USERNAME' #, :optional => false, :multiple => false + command :'add-user' do |c| + + c.switch 'self', :desc => 'Add yourself as a trusted sysadmin by choosing among the public keys available for the current user.', :negatable => false + c.flag 'ssh-pub-key', :desc => 'SSH public key file for this new user' + c.flag 'pgp-pub-key', :desc => 'OpenPGP public key file for this new user' + + c.action do |global_options,options,args| + username = args.first + if !username.any? + if options[:self] + username ||= `whoami`.strip + else + help! "Either USERNAME argument or --self flag is required." + end + end + if Leap::Platform.reserved_usernames.include? username + bail! %(The username "#{username}" is reserved. Sorry, pick another.) + end + + ssh_pub_key = nil + pgp_pub_key = nil + + if options['ssh-pub-key'] + ssh_pub_key = read_file!(options['ssh-pub-key']) + end + if options['pgp-pub-key'] + pgp_pub_key = read_file!(options['pgp-pub-key']) + end + + if options[:self] + ssh_pub_key ||= pick_ssh_key.to_s + pgp_pub_key ||= pick_pgp_key + end + + assert!(ssh_pub_key, 'Sorry, could not find SSH public key.') + + if ssh_pub_key + write_file!([:user_ssh, username], ssh_pub_key) + end + if pgp_pub_key + write_file!([:user_pgp, username], pgp_pub_key) + end + + update_authorized_keys + end + end + + # + # let the the user choose among the ssh public keys that we encounter, or just pick the key if there is only one. + # + def pick_ssh_key + ssh_keys = [] + Dir.glob("#{ENV['HOME']}/.ssh/*.pub").each do |keyfile| + ssh_keys << SshKey.load(keyfile) + end + + if `which ssh-add`.strip.any? + `ssh-add -L 2> /dev/null`.split("\n").compact.each do |line| + key = SshKey.load(line) + if key + key.comment = 'ssh-agent' + ssh_keys << key unless ssh_keys.include?(key) + end + end + end + ssh_keys.compact! + + assert! ssh_keys.any?, 'Sorry, could not find any SSH public key for you. Have you run ssh-keygen?' + + if ssh_keys.length > 1 + key_index = numbered_choice_menu('Choose your SSH public key', ssh_keys.collect(&:summary)) do |line, i| + say("#{i+1}. #{line}") + end + else + key_index = 0 + end + + return ssh_keys[key_index] + end + + # + # let the the user choose among the gpg public keys that we encounter, or just pick the key if there is only one. + # + def pick_pgp_key + begin + require 'gpgme' + rescue LoadError + log "Skipping OpenPGP setup because gpgme is not installed." + return + end + + secret_keys = GPGME::Key.find(:secret) + if secret_keys.empty? + log "Skipping OpenPGP setup because I could not find any OpenPGP keys for you" + return nil + end + + secret_keys.select!{|key| !key.expired} + + if secret_keys.length > 1 + key_index = numbered_choice_menu('Choose your OpenPGP public key', secret_keys) do |key, i| + key_info = key.to_s.split("\n")[0..1].map{|line| line.sub(/^\s*(sec|uid)\s*/,'')}.join(' -- ') + say("#{i+1}. #{key_info}") + end + else + key_index = 0 + end + + key_id = secret_keys[key_index].sha + + # can't use this, it includes signatures: + #puts GPGME::Key.export(key_id, :armor => true, :export_options => :export_minimal) + + # export with signatures removed: + return `gpg --armor --export-options export-minimal --export #{key_id}`.strip + end + + end +end diff --git a/lib/leap_cli/commands/util.rb b/lib/leap_cli/commands/util.rb new file mode 100644 index 00000000..c1da570e --- /dev/null +++ b/lib/leap_cli/commands/util.rb @@ -0,0 +1,50 @@ +module LeapCli; module Commands + + extend self + extend LeapCli::Util + extend LeapCli::Util::RemoteCommand + + def path(name) + Path.named_path(name) + end + + # + # keeps prompting the user for a numbered choice, until they pick a good one or bail out. + # + # block is yielded and is responsible for rendering the choices. + # + def numbered_choice_menu(msg, items, &block) + while true + say("\n" + msg + ':') + items.each_with_index &block + say("q. quit") + index = ask("number 1-#{items.length}> ") + if index.empty? + next + elsif index =~ /q/ + bail! + else + i = index.to_i - 1 + if i < 0 || i >= items.length + bail! + else + return i + end + end + end + end + + + def parse_node_list(nodes) + if nodes.is_a? Config::Object + Config::ObjectList.new(nodes) + elsif nodes.is_a? Config::ObjectList + nodes + elsif nodes.is_a? String + manager.filter!(nodes) + else + bail! "argument error" + end + end + +end; end diff --git a/lib/leap_cli/commands/vagrant.rb b/lib/leap_cli/commands/vagrant.rb new file mode 100644 index 00000000..9fdd48e3 --- /dev/null +++ b/lib/leap_cli/commands/vagrant.rb @@ -0,0 +1,180 @@ +autoload :IPAddr, 'ipaddr' +require 'fileutils' + +module LeapCli; module Commands + + desc "Manage local virtual machines." + long_desc "This command provides a convient way to manage Vagrant-based virtual machines. If FILTER argument is missing, the command runs on all local virtual machines. The Vagrantfile is automatically generated in 'test/Vagrantfile'. If you want to run vagrant commands manually, cd to 'test'." + command [:local, :l] do |local| + local.desc 'Starts up the virtual machine(s)' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :start do |start| + start.flag(:basebox, + :desc => "The basebox to use. This value is passed to vagrant as the "+ + "`config.vm.box` option. The value here should be the name of an installed box or a "+ + "shorthand name of a box in HashiCorp's Atlas.", + :arg_name => 'BASEBOX', + :default_value => 'LEAP/jessie' + ) + start.action do |global_options,options,args| + vagrant_command(["up", "sandbox on"], args, options) + end + end + + local.desc 'Shuts down the virtual machine(s)' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :stop do |stop| + stop.action do |global_options,options,args| + if global_options[:yes] + vagrant_command("halt --force", args) + else + vagrant_command("halt", args) + end + end + end + + local.desc 'Destroys the virtual machine(s), reclaiming the disk space' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :destroy do |destroy| + destroy.action do |global_options,options,args| + if global_options[:yes] + vagrant_command("destroy --force", args) + else + vagrant_command("destroy", args) + end + end + end + + local.desc 'Print the status of local virtual machine(s)' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :status do |status| + status.action do |global_options,options,args| + vagrant_command("status", args) + end + end + + local.desc 'Saves the current state of the virtual machine as a new snapshot' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :save do |status| + status.action do |global_options,options,args| + vagrant_command("sandbox commit", args) + end + end + + local.desc 'Resets virtual machine(s) to the last saved snapshot' + local.arg_name 'FILTER', :optional => true #, :multiple => false + local.command :reset do |reset| + reset.action do |global_options,options,args| + vagrant_command("sandbox rollback", args) + end + end + end + + public + + # + # returns the path to a vagrant ssh private key file. + # + # if the vagrant.key file is owned by root or ourselves, then + # we need to make sure that it owned by us and not world readable. + # + def vagrant_ssh_key_file + file_path = Path.vagrant_ssh_priv_key_file + Util.assert_files_exist! file_path + uid = File.new(file_path).stat.uid + if uid == 0 || uid == Process.euid + FileUtils.install file_path, '/tmp/vagrant.key', :mode => 0600 + file_path = '/tmp/vagrant.key' + end + return file_path + end + + protected + + def vagrant_command(cmds, args, options={}) + vagrant_setup(options) + cmds = cmds.to_a + if args.empty? + nodes = [""] + else + nodes = manager.filter(args)[:environment => "local"].field(:name) + end + if nodes.any? + vagrant_dir = File.dirname(Path.named_path(:vagrantfile)) + exec = ["cd #{vagrant_dir}"] + cmds.each do |cmd| + nodes.each do |node| + exec << "vagrant #{cmd} #{node}" + end + end + execute exec.join('; ') + else + bail! "No nodes found. This command only works on nodes with ip_address in the network #{LeapCli.leapfile.vagrant_network}" + end + end + + private + + def vagrant_setup(options) + assert_bin! 'vagrant', 'Vagrant is required for running local virtual machines. Run "sudo apt-get install vagrant".' + assert! (vagrant_version >= Gem::Version.new('1.1')), 'Vagrant version >= 1.1 is required for running local virtual machines. Please upgrade.' + + unless assert_run!('vagrant plugin list | grep sahara | cat').chars.any? + log :installing, "vagrant plugin 'sahara'" + assert_run! 'vagrant plugin install sahara' + end + create_vagrant_file(options) + end + + def vagrant_version + @vagrant_version ||= Gem::Version.new(assert_run!('vagrant --version').split(' ')[1]) + end + + def execute(cmd) + log 2, :run, cmd + exec cmd + end + + def create_vagrant_file(options) + lines = [] + + basebox = options[:basebox] || 'LEAP/jessie' + # override basebox with custom setting from Leapfile or ~/.leaprc + basebox = leapfile.vagrant_basebox || basebox + + lines << %[Vagrant.configure("2") do |config|] + manager.each_node do |node| + if node.vagrant? + lines << %[ config.vm.define :#{node.name} do |config|] + lines << %[ config.vm.box = "#{basebox}"] + lines << %[ config.vm.network :private_network, ip: "#{node.ip_address}"] + lines << %[ config.vm.provider "virtualbox" do |v|] + lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]] + lines << %[ v.name = "#{node.name}"] + lines << %[ v.memory = 1536] + lines << %[ end] + lines << %[ config.vm.provider "libvirt" do |v|] + lines << %[ v.memory = 1536] + lines << %[ end] + lines << %[ #{leapfile.custom_vagrant_vm_line}] if leapfile.custom_vagrant_vm_line + lines << %[ end] + end + end + + lines << %[end] + lines << "" + write_file! :vagrantfile, lines.join("\n") + end + + def pick_next_vagrant_ip_address + taken_ips = manager.nodes[:environment => "local"].field(:ip_address) + if taken_ips.any? + highest_ip = taken_ips.map{|ip| IPAddr.new(ip)}.max + new_ip = highest_ip.succ + else + new_ip = IPAddr.new(LeapCli.leapfile.vagrant_network).succ.succ + end + return new_ip.to_s + end + +end; end diff --git a/lib/leap_cli/macros.rb b/lib/leap_cli/macros.rb new file mode 100644 index 00000000..fdb9a94e --- /dev/null +++ b/lib/leap_cli/macros.rb @@ -0,0 +1,16 @@ +# +# MACROS +# +# The methods in these files are available in the context of a .json configuration file. +# (The module LeapCli::Macro is included in Config::Object) +# + +require_relative 'macros/core' +require_relative 'macros/files' +require_relative 'macros/haproxy' +require_relative 'macros/hosts' +require_relative 'macros/keys' +require_relative 'macros/nodes' +require_relative 'macros/secrets' +require_relative 'macros/stunnel' +require_relative 'macros/provider' diff --git a/lib/leap_cli/macros/core.rb b/lib/leap_cli/macros/core.rb new file mode 100644 index 00000000..873da358 --- /dev/null +++ b/lib/leap_cli/macros/core.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 + +module LeapCli + module Macro + + # + # Creates a hash from the ssh key info in users directory, for use in + # updating authorized_keys file. Additionally, the 'monitor' public key is + # included, which is used by the monitor nodes to run particular commands + # remotely. + # + def authorized_keys + hash = {} + keys = Dir.glob(Path.named_path([:user_ssh, '*'])) + keys.sort.each do |keyfile| + ssh_type, ssh_key = File.read(keyfile, :encoding => 'UTF-8').strip.split(" ") + name = File.basename(File.dirname(keyfile)) + until hash[name].nil? + i ||= 1; name = "#{name}#{i+=1}" + end + hash[name] = { + "type" => ssh_type, + "key" => ssh_key + } + end + ssh_type, ssh_key = File.read(Path.named_path(:monitor_pub_key), :encoding => 'UTF-8').strip.split(" ") + hash[Leap::Platform.monitor_username] = { + "type" => ssh_type, + "key" => ssh_key + } + hash + end + + def assert(assertion) + if instance_eval(assertion) + true + else + raise AssertionFailed.new(assertion), assertion, caller + end + end + + def error(msg) + raise ConfigError.new(@node, msg), msg, caller + end + + # + # applies a JSON partial to this node + # + def apply_partial(partial_path) + if env.partials[partial_path] + self.deep_merge!(env.partials[partial_path]) + else + raise ArgumentError.new( + "No such partial `%s`. Available partials include:\n%s" % + [partial_path, env.partials.keys.join(", ")] + ) + end + end + + # + # If at first you don't succeed, then it is time to give up. + # + # try{} returns nil if anything in the block throws an exception. + # + # You can wrap something that might fail in `try`, like so. + # + # "= try{ nodes[:services => 'tor'].first.ip_address } " + # + def try(&block) + yield + rescue NoMethodError + rescue ArgumentError + nil + end + + protected + + # + # returns a node list, if argument is not already one + # + def listify(node_list) + if node_list.is_a? Config::ObjectList + node_list + elsif node_list.is_a? Config::Object + Config::ObjectList.new(node_list) + else + raise ArgumentError, 'argument must be a node or node list, not a `%s`' % node_list.class, caller + end + end + + end +end diff --git a/lib/leap_cli/macros/files.rb b/lib/leap_cli/macros/files.rb new file mode 100644 index 00000000..04c94edf --- /dev/null +++ b/lib/leap_cli/macros/files.rb @@ -0,0 +1,124 @@ +# encoding: utf-8 + +## +## FILES +## + +module LeapCli + module Macro + + # + # inserts the contents of a file + # + def file(filename, options={}) + if filename.is_a? Symbol + filename = [filename, @node.name] + end + filepath = Path.find_file(filename) + if filepath + if filepath =~ /\.erb$/ + return ERB.new(File.read(filepath, :encoding => 'UTF-8'), nil, '%<>').result(binding) + else + return File.read(filepath, :encoding => 'UTF-8') + end + else + raise FileMissing.new(Path.named_path(filename), options) + end + end + + # + # like #file, but allow missing files + # + def try_file(filename) + return file(filename) + rescue FileMissing + return nil + end + + # + # returns the location of a file that is stored on the local + # host, under PROVIDER_DIR/files. + # + def local_file_path(path, options={}) + if path.is_a? Symbol + path = [path, @node.name] + elsif path.is_a? String + # ensure it prefixed with files/ + unless path =~ /^files\// + path = "files/" + path + end + end + local_path = Path.find_file(path) + if local_path.nil? + if options[:missing] + raise FileMissing.new(Path.named_path(path), options) + elsif block_given? + yield + return local_file_path(path, options) # try again. + else + Util::log 2, :skipping, "local_file_path(\"#{path}\") because there is no such file." + return nil + end + else + return local_path + end + end + + # + # Returns the location of a file once it is deployed via rsync to the a + # remote server. An internal list of discovered file paths is saved, in + # order to rsync these files when needed. + # + # If the file does not exist, nil is returned. + # + # If there is a block given and the file does not actually exist, the + # block will be yielded to give an opportunity for some code to create the + # file. + # + # For example: + # + # file_path(:dkim_priv_key) {generate_dkim_key} + # + # notes: + # + # * argument 'path' is relative to Path.provider/files or + # Path.provider_base/files + # * the path returned by this method is absolute + # * the path stored for use later by rsync is relative to Path.provider + # * if the path does not exist locally, but exists in provider_base, + # then the default file from provider_base is copied locally. this + # is required for rsync to work correctly. + # + def remote_file_path(path, options={}, &block) + local_path = local_file_path(path, options, &block) + + return nil if local_path.nil? + + # if file is under Path.provider_base, we must copy the default file to + # to Path.provider in order for rsync to be able to sync the file. + if local_path =~ /^#{Regexp.escape(Path.provider_base)}/ + local_provider_path = local_path.sub(/^#{Regexp.escape(Path.provider_base)}/, Path.provider) + FileUtils.mkdir_p File.dirname(local_provider_path), :mode => 0700 + FileUtils.install local_path, local_provider_path, :mode => 0600 + Util.log :created, Path.relative_path(local_provider_path) + local_path = local_provider_path + end + + # ensure directories end with /, important for building rsync command + if File.directory?(local_path) && local_path !~ /\/$/ + local_path += '/' + end + + relative_path = Path.relative_path(local_path) + relative_path.sub!(/^files\//, '') # remove "files/" prefix + @node.file_paths << relative_path + return File.join(Leap::Platform.files_dir, relative_path) + end + + # deprecated + def file_path(path, options={}) + return remote_file_path(path, options) + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/macros/haproxy.rb b/lib/leap_cli/macros/haproxy.rb new file mode 100644 index 00000000..602ae726 --- /dev/null +++ b/lib/leap_cli/macros/haproxy.rb @@ -0,0 +1,73 @@ +# encoding: utf-8 + +## +## HAPROXY +## + +module LeapCli + module Macro + + # + # creates a hash suitable for configuring haproxy. the key is the node name of the server we are proxying to. + # + # * node_list - a hash of nodes for the haproxy servers + # * stunnel_client - contains the mappings to local ports for each server node. + # * non_stunnel_port - in case self is included in node_list, the port to connect to. + # + # 1000 weight is used for nodes in the same location. + # 100 otherwise. + # + def haproxy_servers(node_list, stunnel_clients, non_stunnel_port=nil) + default_weight = 10 + local_weight = 100 + + # record the hosts_file + hostnames(node_list) + + # create a simple map for node name -> local stunnel accept port + accept_ports = stunnel_clients.inject({}) do |hsh, stunnel_entry| + name = stunnel_entry.first.sub /_[0-9]+$/, '' + hsh[name] = stunnel_entry.last['accept_port'] + hsh + end + + # if one the nodes in the node list is ourself, then there will not be a stunnel to it, + # but we need to include it anyway in the haproxy config. + if node_list[self.name] && non_stunnel_port + accept_ports[self.name] = non_stunnel_port + end + + # create the first pass of the servers hash + servers = node_list.values.inject(Config::ObjectList.new) do |hsh, node| + # make sure we have a port to talk to + unless accept_ports[node.name] + error "haproxy needs a local port to talk to when connecting to #{node.name}" + end + weight = default_weight + try { + weight = local_weight if self.location.name == node.location.name + } + hsh[node.name] = Config::Object[ + 'backup', false, + 'host', 'localhost', + 'port', accept_ports[node.name], + 'weight', weight + ] + if node.services.include?('couchdb') + hsh[node.name]['writable'] = node.couch.mode != 'mirror' + end + hsh + end + + # if there are some local servers, make the others backup + if servers.detect{|k,v| v.weight == local_weight} + servers.each do |k,server| + server['backup'] = server['weight'] == default_weight + end + end + + return servers + end + + end +end diff --git a/lib/leap_cli/macros/hosts.rb b/lib/leap_cli/macros/hosts.rb new file mode 100644 index 00000000..963857ae --- /dev/null +++ b/lib/leap_cli/macros/hosts.rb @@ -0,0 +1,90 @@ +# encoding: utf-8 + +module LeapCli + module Macro + + ## + ## IPs + ## + + # + # returns a simple array of all the IPs for the specified node list + # + def host_ips(node_list) + if self.vagrant? + node_list = node_list['environment' => 'local'] + else + node_list = node_list['environment' => '!local'] + end + node_list.map {|name, n| + [n.ip_address, (manager.facts[name]||{})['ec2_public_ipv4']] + }.flatten.compact.uniq + end + + ## + ## HOSTS + ## + + # + # records the list of hosts that are encountered for this node + # + def hostnames(nodes) + @referenced_nodes ||= Config::ObjectList.new + nodes = listify(nodes) + nodes.each_node do |node| + @referenced_nodes[node.name] ||= node + end + return nodes.values.collect {|node| node.domain.name} + end + + # + # Generates entries needed for updating /etc/hosts on a node (as a hash). + # + # Argument `nodes` can be nil or a list of nodes. If nil, only include the + # IPs of the other nodes this @node as has encountered (plus all mx nodes). + # + # Also, for virtual machines, we use the local address if this @node is in + # the same location as the node in question. + # + # We include the ssh public key for each host, so that the hash can also + # be used to generate the /etc/ssh/known_hosts + # + def hosts_file(nodes=nil) + if nodes.nil? + if @referenced_nodes && @referenced_nodes.any? + nodes = @referenced_nodes + nodes = nodes.merge(nodes_like_me[:services => 'mx']) # all nodes always need to communicate with mx nodes. + end + end + return {} unless nodes + hosts = {} + my_location = @node['location'] ? @node['location']['name'] : nil + nodes.each_node do |node| + hosts[node.name] = { + 'ip_address' => node.ip_address, + 'domain_internal' => node.domain.internal, + 'domain_full' => node.domain.full, + 'port' => node.ssh.port + } + if node.dns['aliases'] && node.dns['aliases'].any? + # include aliases, but without domain.full + hosts[node.name]['aliases'] = node.dns['aliases'] - [node.domain.full] + end + node_location = node['location'] ? node['location']['name'] : nil + if my_location == node_location + if facts = @node.manager.facts[node.name] + if facts['ec2_public_ipv4'] + hosts[node.name]['ip_address'] = facts['ec2_public_ipv4'] + end + end + end + host_pub_key = Util::read_file([:node_ssh_pub_key,node.name]) + if host_pub_key + hosts[node.name]['host_pub_key'] = host_pub_key + end + end + hosts + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/macros/keys.rb b/lib/leap_cli/macros/keys.rb new file mode 100644 index 00000000..e7a75cfb --- /dev/null +++ b/lib/leap_cli/macros/keys.rb @@ -0,0 +1,97 @@ +# encoding: utf-8 + +# +# Macro for dealing with cryptographic keys +# + +module LeapCli + module Macro + + # + # return a fingerprint for a key or certificate + # + def fingerprint(filename, options={}) + options[:mode] ||= :x509 + if options[:mode] == :x509 + "SHA256: " + X509.fingerprint("SHA256", Path.named_path(filename)) + elsif options[:mode] == :rsa + key = OpenSSL::PKey::RSA.new(File.read(filename)) + Digest::SHA1.new.hexdigest(key.to_der) + end + end + + ## + ## TOR + ## + + # + # return the path to the tor public key + # generating key if it is missing + # + def tor_public_key_path(path_name, key_type) + file_path(path_name) { generate_tor_key(key_type) } + end + + # + # return the path to the tor private key + # generating key if it is missing + # + def tor_private_key_path(path_name, key_type) + file_path(path_name) { generate_tor_key(key_type) } + end + + # + # Generates a onion_address from a public RSA key file. + # + # path_name is the named path of the Tor public key. + # + # Basically, an onion address is nothing more than a base32 encoding + # of the first 10 bytes of a sha1 digest of the public key. + # + # Additionally, Tor ignores the 22 byte header of the public key + # before taking the sha1 digest. + # + def onion_address(path_name) + require 'base32' + require 'base64' + require 'openssl' + path = Path.find_file([path_name, self.name]) + if path && File.exists?(path) + public_key_str = File.readlines(path).grep(/^[^-]/).join + public_key = Base64.decode64(public_key_str) + public_key = public_key.slice(22..-1) # Tor ignores the 22 byte SPKI header + sha1sum = Digest::SHA1.new.digest(public_key) + Base32.encode(sha1sum.slice(0,10)).downcase + else + LeapCli.log :warning, 'Tor public key file "%s" does not exist' % tor_public_key_path + end + end + + def generate_dkim_key(bit_size=2048) + LeapCli.log :generating, "%s bit RSA DKIM key" % bit_size do + private_key = OpenSSL::PKey::RSA.new(bit_size) + public_key = private_key.public_key + LeapCli::Util.write_file! :dkim_priv_key, private_key.to_pem + LeapCli::Util.write_file! :dkim_pub_key, public_key.to_pem + end + end + + private + + def generate_tor_key(key_type) + if key_type == 'RSA' + require 'certificate_authority' + keypair = CertificateAuthority::MemoryKeyMaterial.new + bit_size = 1024 + LeapCli.log :generating, "%s bit RSA Tor key" % bit_size do + keypair.generate_key(bit_size) + LeapCli::Util.write_file! [:node_tor_priv_key, self.name], keypair.private_key.to_pem + LeapCli::Util.write_file! [:node_tor_pub_key, self.name], keypair.public_key.to_pem + end + else + LeapCli.bail! 'tor.key.type of %s is not yet supported' % key_type + end + end + + end +end diff --git a/lib/leap_cli/macros/nodes.rb b/lib/leap_cli/macros/nodes.rb new file mode 100644 index 00000000..0e23831d --- /dev/null +++ b/lib/leap_cli/macros/nodes.rb @@ -0,0 +1,88 @@ +# encoding: utf-8 + +## +## node related macros +## + +module LeapCli + module Macro + + # + # the list of all the nodes + # + def nodes + env.nodes + end + + # + # simple alias for global.provider + # + def provider + env.provider + end + + # + # returns a list of nodes that match the same environment + # + # if @node.environment is not set, we return other nodes + # where environment is not set. + # + def nodes_like_me + nodes[:environment => @node.environment] + end + + # + # returns a list of nodes that match the location name + # and environment of @node. + # + def nodes_near_me + if @node['location'] && @node['location']['name'] + nodes_like_me['location.name' => @node.location.name] + else + nodes_like_me['location' => nil] + end + end + + # + # + # picks a node out from the node list in such a way that: + # + # (1) which nodes picked which nodes is saved in secrets.json + # (2) when other nodes call this macro with the same node list, they are guaranteed to get a different node + # (3) if all the nodes in the pick_node list have been picked, remaining nodes are distributed randomly. + # + # if the node_list is empty, an exception is raised. + # if node_list size is 1, then that node is returned and nothing is + # memorized via the secrets.json file. + # + # `label` is needed to distinguish between pools of nodes for different purposes. + # + # TODO: more evenly balance after all the nodes have been picked. + # + def pick_node(label, node_list) + if node_list.any? + if node_list.size == 1 + return node_list.values.first + else + secrets_key = "pick_node(:#{label},#{node_list.keys.sort.join(',')})" + secrets_value = @manager.secrets.retrieve(secrets_key, @node.environment) || {} + secrets_value[@node.name] ||= begin + node_to_pick = nil + node_list.each_node do |node| + next if secrets_value.values.include?(node.name) + node_to_pick = node.name + end + node_to_pick ||= secrets_value.values.shuffle.first # all picked already, so pick a random one. + node_to_pick + end + picked_node_name = secrets_value[@node.name] + @manager.secrets.set(secrets_key, secrets_value, @node.environment) + return node_list[picked_node_name] + end + else + raise ArgumentError.new('pick_node(node_list): node_list cannot be empty') + end + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/macros/provider.rb b/lib/leap_cli/macros/provider.rb new file mode 100644 index 00000000..4e74da01 --- /dev/null +++ b/lib/leap_cli/macros/provider.rb @@ -0,0 +1,90 @@ +# +# These macros are intended only for use in provider.json, although they are +# currently loaded in all .json contexts. +# + +module LeapCli + module Macro + + # + # returns an array of the service names, including only those services that + # are enabled for this environment. + # + def enabled_services + manager.env(self.environment).services[:service_type => :user_service].field(:name).select { |service| + manager.nodes[:environment => self.environment][:services => service].any? + } + end + + # + # The webapp will not work unless the service level configuration is precisely defined. + # Here, we take what the sysadmin has specified in provider.json and clean it up to + # ensure it is OK. + # + # It would be better to add support for JSON schema. + # + def service_levels() + levels = {} + provider.service.levels.each do |name, level| + if name =~ /^[0-9]+$/ + name = name.to_i + end + levels[name] = level_cleanup(name, level.clone) + end + levels + end + + private + + def print_warning(name, msg) + if self.environment + provider_str = "provider.json or %s" % ['provider', self.environment, 'json'].join('.') + else + provider_str = "provider.json" + end + LeapCli::log :warning, "In #{provider_str}, you have an incorrect definition for service level '#{name}':" do + LeapCli::log msg + end + end + + def level_cleanup(name, level) + unless level['name'] + print_warning(name, 'required field "name" is missing') + end + unless level['description'] + print_warning(name, 'required field "description" is missing') + end + unless level['bandwidth'].nil? || level['bandwidth'] == 'limited' + print_warning(name, 'field "bandwidth" must be nil or "limited"') + end + unless level['rate'].nil? || level['rate'].is_a?(Hash) + print_warning(name, 'field "rate" must be nil or a hash (e.g. {"USD":10, "EUR":10})') + end + possible_services = enabled_services + if level['services'] + level['services'].each do |service| + unless possible_services.include? service + print_warning(name, "the service '#{service}' does not exist or there are no nodes that provide this service.") + LeapCli::Util::bail! + end + end + else + level['services'] = possible_services + end + level['services'] = remap_services(level['services']) + level + end + + # + # the service names that the webapp uses and that leap_platform uses are different. ugh. + # + SERVICE_MAP = { + "mx" => "email", + "openvpn" => "eip" + } + def remap_services(services) + services.map {|srv| SERVICE_MAP[srv]} + end + + end +end diff --git a/lib/leap_cli/macros/secrets.rb b/lib/leap_cli/macros/secrets.rb new file mode 100644 index 00000000..8d1feb55 --- /dev/null +++ b/lib/leap_cli/macros/secrets.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +require 'base32' + +module LeapCli + module Macro + + # + # inserts a named secret, generating it if needed. + # + # manager.export_secrets should be called later to capture any newly generated secrets. + # + # +length+ is the character length of the generated password. + # + def secret(name, length=32) + manager.secrets.set(name, @node.environment) { Util::Secret.generate(length) } + end + + # inserts a base32 encoded secret + def base32_secret(name, length=20) + manager.secrets.set(name, @node.environment) { Base32.encode(Util::Secret.generate(length)) } + end + + # Picks a random obfsproxy port from given range + def rand_range(name, range) + manager.secrets.set(name, @node.environment) { rand(range) } + end + + # + # inserts an hexidecimal secret string, generating it if needed. + # + # +bit_length+ is the bits in the secret, (ie length of resulting hex string will be bit_length/4) + # + def hex_secret(name, bit_length=128) + manager.secrets.set(name, @node.environment) { Util::Secret.generate_hex(bit_length) } + end + + end +end
\ No newline at end of file diff --git a/lib/leap_cli/macros/stunnel.rb b/lib/leap_cli/macros/stunnel.rb new file mode 100644 index 00000000..821bda38 --- /dev/null +++ b/lib/leap_cli/macros/stunnel.rb @@ -0,0 +1,106 @@ +## +## STUNNEL +## + +# +# About stunnel +# -------------------------- +# +# The network looks like this: +# +# From the client's perspective: +# +# |------- stunnel client --------------| |---------- stunnel server -----------------------| +# consumer app -> localhost:accept_port -> connect:connect_port -> ?? +# +# From the server's perspective: +# +# |------- stunnel client --------------| |---------- stunnel server -----------------------| +# ?? -> *:accept_port -> localhost:connect_port -> service +# + +module LeapCli + module Macro + + # + # stunnel configuration for the client side. + # + # +node_list+ is a ObjectList of nodes running stunnel servers. + # + # +port+ is the real port of the ultimate service running on the servers + # that the client wants to connect to. + # + # * accept_port is the port on localhost to which local clients + # can connect. it is auto generated serially. + # + # * connect_port is the port on the stunnel server to connect to. + # it is auto generated from the +port+ argument. + # + # generates an entry appropriate to be passed directly to + # create_resources(stunnel::service, hiera('..'), defaults) + # + # local ports are automatically generated, starting at 4000 + # and incrementing in sorted order (by node name). + # + def stunnel_client(node_list, port, options={}) + @next_stunnel_port ||= 4000 + node_list = listify(node_list) + hostnames(node_list) # record the hosts + result = Config::ObjectList.new + node_list.each_node do |node| + if node.name != self.name || options[:include_self] + s_port = stunnel_port(port) + result["#{node.name}_#{port}"] = Config::Object[ + 'accept_port', @next_stunnel_port, + 'connect', node.domain.internal, + 'connect_port', s_port, + 'original_port', port + ] + manager.connections.add(:from => @node.ip_address, :to => node.ip_address, :port => s_port) + @next_stunnel_port += 1 + end + end + result + end + + # + # generates a stunnel server entry. + # + # +port+ is the real port targeted service. + # + # * `accept_port` is the publicly bound port + # * `connect_port` is the port that the local service is running on. + # + def stunnel_server(port) + { + "accept_port" => stunnel_port(port), + "connect_port" => port + } + end + + # + # lists the ips that connect to this node, on particular ports. + # + def stunnel_firewall + manager.connections.select {|connection| + connection['to'] == @node.ip_address + } + end + + private + + # + # maps a real port to a stunnel port (used as the connect_port in the client config + # and the accept_port in the server config) + # + def stunnel_port(port) + port = port.to_i + if port < 50000 + return port + 10000 + else + return port - 10000 + end + end + + end +end
\ No newline at end of file diff --git a/lib/puppet/functions/type_of.rb b/lib/puppet/functions/type_of.rb deleted file mode 100644 index 02cdd4db..00000000 --- a/lib/puppet/functions/type_of.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Returns the type when passed a value. -# -# @example how to compare values' types -# # compare the types of two values -# if type_of($first_value) != type_of($second_value) { fail("first_value and second_value are different types") } -# @example how to compare against an abstract type -# unless type_of($first_value) <= Numeric { fail("first_value must be Numeric") } -# unless type_of{$first_value) <= Collection[1] { fail("first_value must be an Array or Hash, and contain at least one element") } -# -# See the documentation for "The Puppet Type System" for more information about types. -# See the `assert_type()` function for flexible ways to assert the type of a value. -# -Puppet::Functions.create_function(:type_of) do - def type_of(value) - Puppet::Pops::Types::TypeCalculator.infer_set(value) - end -end diff --git a/lib/puppet/parser/functions/abs.rb b/lib/puppet/parser/functions/abs.rb deleted file mode 100644 index 11d2d7fe..00000000 --- a/lib/puppet/parser/functions/abs.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -# abs.rb -# - -module Puppet::Parser::Functions - newfunction(:abs, :type => :rvalue, :doc => <<-EOS - Returns the absolute value of a number, for example -34.56 becomes - 34.56. Takes a single integer and float value as an argument. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "abs(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - # Numbers in Puppet are often string-encoded which is troublesome ... - if value.is_a?(String) - if value.match(/^-?(?:\d+)(?:\.\d+){1}$/) - value = value.to_f - elsif value.match(/^-?\d+$/) - value = value.to_i - else - raise(Puppet::ParseError, 'abs(): Requires float or ' + - 'integer to work with') - end - end - - # We have numeric value to handle ... - result = value.abs - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/any2array.rb b/lib/puppet/parser/functions/any2array.rb deleted file mode 100644 index e71407e8..00000000 --- a/lib/puppet/parser/functions/any2array.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# any2array.rb -# - -module Puppet::Parser::Functions - newfunction(:any2array, :type => :rvalue, :doc => <<-EOS -This converts any object to an array containing that object. Empty argument -lists are converted to an empty array. Arrays are left untouched. Hashes are -converted to arrays of alternating keys and values. - EOS - ) do |arguments| - - if arguments.empty? - return [] - end - - if arguments.length == 1 - if arguments[0].kind_of?(Array) - return arguments[0] - elsif arguments[0].kind_of?(Hash) - result = [] - arguments[0].each do |key, value| - result << key << value - end - return result - end - end - - return arguments - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/base64.rb b/lib/puppet/parser/functions/base64.rb deleted file mode 100644 index 617ba31b..00000000 --- a/lib/puppet/parser/functions/base64.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:base64, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - - Base64 encode or decode a string based on the command and the string submitted - - Usage: - - $encodestring = base64('encode','thestring') - $decodestring = base64('decode','dGhlc3RyaW5n') - - ENDHEREDOC - - require 'base64' - - raise Puppet::ParseError, ("base64(): Wrong number of arguments (#{args.length}; must be = 2)") unless args.length == 2 - - actions = ['encode','decode'] - - unless actions.include?(args[0]) - raise Puppet::ParseError, ("base64(): the first argument must be one of 'encode' or 'decode'") - end - - unless args[1].is_a?(String) - raise Puppet::ParseError, ("base64(): the second argument must be a string to base64") - end - - case args[0] - when 'encode' - result = Base64.encode64(args[1]) - when 'decode' - result = Base64.decode64(args[1]) - end - - return result - end -end diff --git a/lib/puppet/parser/functions/basename.rb b/lib/puppet/parser/functions/basename.rb deleted file mode 100644 index f7e44384..00000000 --- a/lib/puppet/parser/functions/basename.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:basename, :type => :rvalue, :doc => <<-EOS - Strips directory (and optional suffix) from a filename - EOS - ) do |arguments| - - if arguments.size < 1 then - raise(Puppet::ParseError, "basename(): No arguments given") - elsif arguments.size > 2 then - raise(Puppet::ParseError, "basename(): Too many arguments given (#{arguments.size})") - else - - unless arguments[0].is_a?(String) - raise(Puppet::ParseError, 'basename(): Requires string as first argument') - end - - if arguments.size == 1 then - rv = File.basename(arguments[0]) - elsif arguments.size == 2 then - - unless arguments[1].is_a?(String) - raise(Puppet::ParseError, 'basename(): Requires string as second argument') - end - - rv = File.basename(arguments[0], arguments[1]) - end - - end - - return rv - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/bool2num.rb b/lib/puppet/parser/functions/bool2num.rb deleted file mode 100644 index 6ad6cf4e..00000000 --- a/lib/puppet/parser/functions/bool2num.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# bool2num.rb -# - -module Puppet::Parser::Functions - newfunction(:bool2num, :type => :rvalue, :doc => <<-EOS - Converts a boolean to a number. Converts the values: - false, f, 0, n, and no to 0 - true, t, 1, y, and yes to 1 - Requires a single boolean or string as an input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "bool2num(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = function_str2bool([arguments[0]]) - - # We have real boolean values as well ... - result = value ? 1 : 0 - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/bool2str.rb b/lib/puppet/parser/functions/bool2str.rb deleted file mode 100644 index fcd37917..00000000 --- a/lib/puppet/parser/functions/bool2str.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# bool2str.rb -# - -module Puppet::Parser::Functions - newfunction(:bool2str, :type => :rvalue, :doc => <<-EOS - Converts a boolean to a string. - Requires a single boolean as an input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "bool2str(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - klass = value.class - - # We can have either true or false, and nothing else - unless [FalseClass, TrueClass].include?(klass) - raise(Puppet::ParseError, 'bool2str(): Requires a boolean to work with') - end - - return value.to_s - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/camelcase.rb b/lib/puppet/parser/functions/camelcase.rb deleted file mode 100644 index d7f43f7a..00000000 --- a/lib/puppet/parser/functions/camelcase.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# camelcase.rb -# - -module Puppet::Parser::Functions - newfunction(:camelcase, :type => :rvalue, :doc => <<-EOS -Converts the case of a string or all strings in an array to camel case. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "camelcase(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - klass = value.class - - unless [Array, String].include?(klass) - raise(Puppet::ParseError, 'camelcase(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.split('_').map{|e| e.capitalize}.join : i } - else - result = value.split('_').map{|e| e.capitalize}.join - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/capitalize.rb b/lib/puppet/parser/functions/capitalize.rb deleted file mode 100644 index 98b2d16c..00000000 --- a/lib/puppet/parser/functions/capitalize.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# capitalize.rb -# - -module Puppet::Parser::Functions - newfunction(:capitalize, :type => :rvalue, :doc => <<-EOS - Capitalizes the first letter of a string or array of strings. - Requires either a single string or an array as an input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "capitalize(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'capitalize(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.capitalize : i } - else - result = value.capitalize - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/chomp.rb b/lib/puppet/parser/functions/chomp.rb deleted file mode 100644 index c55841e3..00000000 --- a/lib/puppet/parser/functions/chomp.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# chomp.rb -# - -module Puppet::Parser::Functions - newfunction(:chomp, :type => :rvalue, :doc => <<-'EOS' - Removes the record separator from the end of a string or an array of - strings, for example `hello\n` becomes `hello`. - Requires a single string or array as an input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "chomp(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'chomp(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.chomp : i } - else - result = value.chomp - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/chop.rb b/lib/puppet/parser/functions/chop.rb deleted file mode 100644 index b24ab785..00000000 --- a/lib/puppet/parser/functions/chop.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -# chop.rb -# - -module Puppet::Parser::Functions - newfunction(:chop, :type => :rvalue, :doc => <<-'EOS' - Returns a new string with the last character removed. If the string ends - with `\r\n`, both characters are removed. Applying chop to an empty - string returns an empty string. If you wish to merely remove record - separators then you should use the `chomp` function. - Requires a string or array of strings as input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "chop(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'chop(): Requires either an ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.chop : i } - else - result = value.chop - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/concat.rb b/lib/puppet/parser/functions/concat.rb deleted file mode 100644 index 618e62d4..00000000 --- a/lib/puppet/parser/functions/concat.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# concat.rb -# - -module Puppet::Parser::Functions - newfunction(:concat, :type => :rvalue, :doc => <<-EOS -Appends the contents of multiple arrays into array 1. - -*Example:* - - concat(['1','2','3'],['4','5','6'],['7','8','9']) - -Would result in: - - ['1','2','3','4','5','6','7','8','9'] - EOS - ) do |arguments| - - # Check that more than 2 arguments have been given ... - raise(Puppet::ParseError, "concat(): Wrong number of arguments " + - "given (#{arguments.size} for < 2)") if arguments.size < 2 - - a = arguments[0] - - # Check that the first parameter is an array - unless a.is_a?(Array) - raise(Puppet::ParseError, 'concat(): Requires array to work with') - end - - result = a - arguments.shift - - arguments.each do |x| - result = result + Array(x) - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/count.rb b/lib/puppet/parser/functions/count.rb deleted file mode 100644 index 52de1b8a..00000000 --- a/lib/puppet/parser/functions/count.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:count, :type => :rvalue, :arity => -2, :doc => <<-EOS -Takes an array as first argument and an optional second argument. -Count the number of elements in array that matches second argument. -If called with only an array it counts the number of elements that are not nil/undef. - EOS - ) do |args| - - if (args.size > 2) then - raise(ArgumentError, "count(): Wrong number of arguments "+ - "given #{args.size} for 1 or 2.") - end - - collection, item = args - - if item then - collection.count item - else - collection.count { |obj| obj != nil && obj != :undef && obj != '' } - end - end -end diff --git a/lib/puppet/parser/functions/deep_merge.rb b/lib/puppet/parser/functions/deep_merge.rb deleted file mode 100644 index 6df32e9c..00000000 --- a/lib/puppet/parser/functions/deep_merge.rb +++ /dev/null @@ -1,44 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:deep_merge, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - Recursively merges two or more hashes together and returns the resulting hash. - - For example: - - $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } - $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } - $merged_hash = deep_merge($hash1, $hash2) - # The resulting hash is equivalent to: - # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } - - When there is a duplicate key that is a hash, they are recursively merged. - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." - - ENDHEREDOC - - if args.length < 2 - raise Puppet::ParseError, ("deep_merge(): wrong number of arguments (#{args.length}; must be at least 2)") - end - - deep_merge = Proc.new do |hash1,hash2| - hash1.merge(hash2) do |key,old_value,new_value| - if old_value.is_a?(Hash) && new_value.is_a?(Hash) - deep_merge.call(old_value, new_value) - else - new_value - end - end - end - - result = Hash.new - args.each do |arg| - next if arg.is_a? String and arg.empty? # empty string is synonym for puppet's undef - # If the argument was not a hash, skip it. - unless arg.is_a?(Hash) - raise Puppet::ParseError, "deep_merge: unexpected argument type #{arg.class}, only expects hash arguments" - end - - result = deep_merge.call(result, arg) - end - return( result ) - end -end diff --git a/lib/puppet/parser/functions/defined_with_params.rb b/lib/puppet/parser/functions/defined_with_params.rb deleted file mode 100644 index d7df306c..00000000 --- a/lib/puppet/parser/functions/defined_with_params.rb +++ /dev/null @@ -1,35 +0,0 @@ -# Test whether a given class or definition is defined -require 'puppet/parser/functions' - -Puppet::Parser::Functions.newfunction(:defined_with_params, - :type => :rvalue, - :doc => <<-'ENDOFDOC' -Takes a resource reference and an optional hash of attributes. - -Returns true if a resource with the specified attributes has already been added -to the catalog, and false otherwise. - - user { 'dan': - ensure => present, - } - - if ! defined_with_params(User[dan], {'ensure' => 'present' }) { - user { 'dan': ensure => present, } - } -ENDOFDOC -) do |vals| - reference, params = vals - raise(ArgumentError, 'Must specify a reference') unless reference - if (! params) || params == '' - params = {} - end - ret = false - if resource = findresource(reference.to_s) - matches = params.collect do |key, value| - resource[key] == value - end - ret = params.empty? || !matches.include?(false) - end - Puppet.debug("Resource #{reference} was not determined to be defined") - ret -end diff --git a/lib/puppet/parser/functions/delete.rb b/lib/puppet/parser/functions/delete.rb deleted file mode 100644 index f548b444..00000000 --- a/lib/puppet/parser/functions/delete.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# delete.rb -# - -# TODO(Krzysztof Wilczynski): We need to add support for regular expression ... - -module Puppet::Parser::Functions - newfunction(:delete, :type => :rvalue, :doc => <<-EOS -Deletes all instances of a given element from an array, substring from a -string, or key from a hash. - -*Examples:* - - delete(['a','b','c','b'], 'b') - Would return: ['a','c'] - - delete({'a'=>1,'b'=>2,'c'=>3}, 'b') - Would return: {'a'=>1,'c'=>3} - - delete({'a'=>1,'b'=>2,'c'=>3}, ['b','c']) - Would return: {'a'=>1} - - delete('abracadabra', 'bra') - Would return: 'acada' - EOS - ) do |arguments| - - if (arguments.size != 2) then - raise(Puppet::ParseError, "delete(): Wrong number of arguments "+ - "given #{arguments.size} for 2.") - end - - collection = arguments[0].dup - Array(arguments[1]).each do |item| - case collection - when Array, Hash - collection.delete item - when String - collection.gsub! item, '' - else - raise(TypeError, "delete(): First argument must be an Array, " + - "String, or Hash. Given an argument of class #{collection.class}.") - end - end - collection - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/delete_at.rb b/lib/puppet/parser/functions/delete_at.rb deleted file mode 100644 index 3eb4b537..00000000 --- a/lib/puppet/parser/functions/delete_at.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# delete_at.rb -# - -module Puppet::Parser::Functions - newfunction(:delete_at, :type => :rvalue, :doc => <<-EOS -Deletes a determined indexed value from an array. - -*Examples:* - - delete_at(['a','b','c'], 1) - -Would return: ['a','c'] - EOS - ) do |arguments| - - raise(Puppet::ParseError, "delete_at(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size < 2 - - array = arguments[0] - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'delete_at(): Requires array to work with') - end - - index = arguments[1] - - if index.is_a?(String) and not index.match(/^\d+$/) - raise(Puppet::ParseError, 'delete_at(): You must provide ' + - 'non-negative numeric index') - end - - result = array.clone - - # Numbers in Puppet are often string-encoded which is troublesome ... - index = index.to_i - - if index > result.size - 1 # First element is at index 0 is it not? - raise(Puppet::ParseError, 'delete_at(): Given index ' + - 'exceeds size of array given') - end - - result.delete_at(index) # We ignore the element that got deleted ... - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/delete_undef_values.rb b/lib/puppet/parser/functions/delete_undef_values.rb deleted file mode 100644 index f94d4da8..00000000 --- a/lib/puppet/parser/functions/delete_undef_values.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:delete_undef_values, :type => :rvalue, :doc => <<-EOS -Returns a copy of input hash or array with all undefs deleted. - -*Examples:* - - $hash = delete_undef_values({a=>'A', b=>'', c=>undef, d => false}) - -Would return: {a => 'A', b => '', d => false} - - $array = delete_undef_values(['A','',undef,false]) - -Would return: ['A','',false] - - EOS - ) do |args| - - raise(Puppet::ParseError, - "delete_undef_values(): Wrong number of arguments given " + - "(#{args.size})") if args.size < 1 - - unless args[0].is_a? Array or args[0].is_a? Hash - raise(Puppet::ParseError, - "delete_undef_values(): expected an array or hash, got #{args[0]} type #{args[0].class} ") - end - result = args[0].dup - if result.is_a?(Hash) - result.delete_if {|key, val| val.equal? :undef} - elsif result.is_a?(Array) - result.delete :undef - end - result - end -end diff --git a/lib/puppet/parser/functions/delete_values.rb b/lib/puppet/parser/functions/delete_values.rb deleted file mode 100644 index f6c8c0e6..00000000 --- a/lib/puppet/parser/functions/delete_values.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:delete_values, :type => :rvalue, :doc => <<-EOS -Deletes all instances of a given value from a hash. - -*Examples:* - - delete_values({'a'=>'A','b'=>'B','c'=>'C','B'=>'D'}, 'B') - -Would return: {'a'=>'A','c'=>'C','B'=>'D'} - - EOS - ) do |arguments| - - raise(Puppet::ParseError, - "delete_values(): Wrong number of arguments given " + - "(#{arguments.size} of 2)") if arguments.size != 2 - - hash, item = arguments - - if not hash.is_a?(Hash) - raise(TypeError, "delete_values(): First argument must be a Hash. " + \ - "Given an argument of class #{hash.class}.") - end - hash.dup.delete_if { |key, val| item == val } - end -end diff --git a/lib/puppet/parser/functions/difference.rb b/lib/puppet/parser/functions/difference.rb deleted file mode 100644 index cd258f75..00000000 --- a/lib/puppet/parser/functions/difference.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -# difference.rb -# - -module Puppet::Parser::Functions - newfunction(:difference, :type => :rvalue, :doc => <<-EOS -This function returns the difference between two arrays. -The returned array is a copy of the original array, removing any items that -also appear in the second array. - -*Examples:* - - difference(["a","b","c"],["b","c","d"]) - -Would return: ["a"] - EOS - ) do |arguments| - - # Two arguments are required - raise(Puppet::ParseError, "difference(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size != 2 - - first = arguments[0] - second = arguments[1] - - unless first.is_a?(Array) && second.is_a?(Array) - raise(Puppet::ParseError, 'difference(): Requires 2 arrays') - end - - result = first - second - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/dirname.rb b/lib/puppet/parser/functions/dirname.rb deleted file mode 100644 index ea8cc1e0..00000000 --- a/lib/puppet/parser/functions/dirname.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:dirname, :type => :rvalue, :doc => <<-EOS - Returns the dirname of a path. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "dirname(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - path = arguments[0] - return File.dirname(path) - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/downcase.rb b/lib/puppet/parser/functions/downcase.rb deleted file mode 100644 index 040b84f5..00000000 --- a/lib/puppet/parser/functions/downcase.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# downcase.rb -# - -module Puppet::Parser::Functions - newfunction(:downcase, :type => :rvalue, :doc => <<-EOS -Converts the case of a string or all strings in an array to lower case. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "downcase(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'downcase(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.downcase : i } - else - result = value.downcase - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/empty.rb b/lib/puppet/parser/functions/empty.rb deleted file mode 100644 index cca620fa..00000000 --- a/lib/puppet/parser/functions/empty.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# empty.rb -# - -module Puppet::Parser::Functions - newfunction(:empty, :type => :rvalue, :doc => <<-EOS -Returns true if the variable is empty. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "empty(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String) - raise(Puppet::ParseError, 'empty(): Requires either ' + - 'array, hash or string to work with') - end - - result = value.empty? - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/ensure_packages.rb b/lib/puppet/parser/functions/ensure_packages.rb deleted file mode 100644 index f1da4aaa..00000000 --- a/lib/puppet/parser/functions/ensure_packages.rb +++ /dev/null @@ -1,35 +0,0 @@ -# -# ensure_packages.rb -# - -module Puppet::Parser::Functions - newfunction(:ensure_packages, :type => :statement, :doc => <<-EOS -Takes a list of packages and only installs them if they don't already exist. -It optionally takes a hash as a second parameter that will be passed as the -third argument to the ensure_resource() function. - EOS - ) do |arguments| - - if arguments.size > 2 or arguments.size == 0 - raise(Puppet::ParseError, "ensure_packages(): Wrong number of arguments " + - "given (#{arguments.size} for 1 or 2)") - elsif arguments.size == 2 and !arguments[1].is_a?(Hash) - raise(Puppet::ParseError, 'ensure_packages(): Requires second argument to be a Hash') - end - - packages = Array(arguments[0]) - - if arguments[1] - defaults = { 'ensure' => 'present' }.merge(arguments[1]) - else - defaults = { 'ensure' => 'present' } - end - - Puppet::Parser::Functions.function(:ensure_resource) - packages.each { |package_name| - function_ensure_resource(['package', package_name, defaults ]) - } - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/ensure_resource.rb b/lib/puppet/parser/functions/ensure_resource.rb deleted file mode 100644 index 1ba6a447..00000000 --- a/lib/puppet/parser/functions/ensure_resource.rb +++ /dev/null @@ -1,46 +0,0 @@ -# Test whether a given class or definition is defined -require 'puppet/parser/functions' - -Puppet::Parser::Functions.newfunction(:ensure_resource, - :type => :statement, - :doc => <<-'ENDOFDOC' -Takes a resource type, title, and a list of attributes that describe a -resource. - - user { 'dan': - ensure => present, - } - -This example only creates the resource if it does not already exist: - - ensure_resource('user', 'dan', {'ensure' => 'present' }) - -If the resource already exists but does not match the specified parameters, -this function will attempt to recreate the resource leading to a duplicate -resource definition error. - -An array of resources can also be passed in and each will be created with -the type and parameters specified if it doesn't already exist. - - ensure_resource('user', ['dan','alex'], {'ensure' => 'present'}) - -ENDOFDOC -) do |vals| - type, title, params = vals - raise(ArgumentError, 'Must specify a type') unless type - raise(ArgumentError, 'Must specify a title') unless title - params ||= {} - - items = [title].flatten - - items.each do |item| - Puppet::Parser::Functions.function(:defined_with_params) - if function_defined_with_params(["#{type}[#{item}]", params]) - Puppet.debug("Resource #{type}[#{item}] with params #{params} not created because it already exists") - else - Puppet.debug("Create new resource #{type}[#{item}] with params #{params}") - Puppet::Parser::Functions.function(:create_resources) - function_create_resources([type.capitalize, { item => params }]) - end - end -end diff --git a/lib/puppet/parser/functions/flatten.rb b/lib/puppet/parser/functions/flatten.rb deleted file mode 100644 index a1ed1832..00000000 --- a/lib/puppet/parser/functions/flatten.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# flatten.rb -# - -module Puppet::Parser::Functions - newfunction(:flatten, :type => :rvalue, :doc => <<-EOS -This function flattens any deeply nested arrays and returns a single flat array -as a result. - -*Examples:* - - flatten(['a', ['b', ['c']]]) - -Would return: ['a','b','c'] - EOS - ) do |arguments| - - raise(Puppet::ParseError, "flatten(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - array = arguments[0] - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'flatten(): Requires array to work with') - end - - result = array.flatten - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/floor.rb b/lib/puppet/parser/functions/floor.rb deleted file mode 100644 index 9a6f014d..00000000 --- a/lib/puppet/parser/functions/floor.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:floor, :type => :rvalue, :doc => <<-EOS - Returns the largest integer less or equal to the argument. - Takes a single numeric value as an argument. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "floor(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - begin - arg = Float(arguments[0]) - rescue TypeError, ArgumentError => e - raise(Puppet::ParseError, "floor(): Wrong argument type " + - "given (#{arguments[0]} for Numeric)") - end - - raise(Puppet::ParseError, "floor(): Wrong argument type " + - "given (#{arg.class} for Numeric)") if arg.is_a?(Numeric) == false - - arg.floor - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/fqdn_rotate.rb b/lib/puppet/parser/functions/fqdn_rotate.rb deleted file mode 100644 index 7f4d37d0..00000000 --- a/lib/puppet/parser/functions/fqdn_rotate.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# fqdn_rotate.rb -# - -module Puppet::Parser::Functions - newfunction(:fqdn_rotate, :type => :rvalue, :doc => <<-EOS -Rotates an array a random number of times based on a nodes fqdn. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "fqdn_rotate(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - require 'digest/md5' - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'fqdn_rotate(): Requires either ' + - 'array or string to work with') - end - - result = value.clone - - string = value.is_a?(String) ? true : false - - # Check whether it makes sense to rotate ... - return result if result.size <= 1 - - # We turn any string value into an array to be able to rotate ... - result = string ? result.split('') : result - - elements = result.size - - srand(Digest::MD5.hexdigest([lookupvar('::fqdn'),arguments].join(':')).hex) - rand(elements).times { - result.push result.shift - } - - result = string ? result.join : result - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/get_module_path.rb b/lib/puppet/parser/functions/get_module_path.rb deleted file mode 100644 index 1421b91f..00000000 --- a/lib/puppet/parser/functions/get_module_path.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:get_module_path, :type =>:rvalue, :doc => <<-EOT - Returns the absolute path of the specified module for the current - environment. - - Example: - $module_path = get_module_path('stdlib') - EOT - ) do |args| - raise(Puppet::ParseError, "get_module_path(): Wrong number of arguments, expects one") unless args.size == 1 - if module_path = Puppet::Module.find(args[0], compiler.environment.to_s) - module_path.path - else - raise(Puppet::ParseError, "Could not find module #{args[0]} in environment #{compiler.environment}") - end - end -end diff --git a/lib/puppet/parser/functions/getparam.rb b/lib/puppet/parser/functions/getparam.rb deleted file mode 100644 index 6d510069..00000000 --- a/lib/puppet/parser/functions/getparam.rb +++ /dev/null @@ -1,35 +0,0 @@ -# Test whether a given class or definition is defined -require 'puppet/parser/functions' - -Puppet::Parser::Functions.newfunction(:getparam, - :type => :rvalue, - :doc => <<-'ENDOFDOC' -Takes a resource reference and name of the parameter and -returns value of resource's parameter. - -*Examples:* - - define example_resource($param) { - } - - example_resource { "example_resource_instance": - param => "param_value" - } - - getparam(Example_resource["example_resource_instance"], "param") - -Would return: param_value -ENDOFDOC -) do |vals| - reference, param = vals - raise(ArgumentError, 'Must specify a reference') unless reference - raise(ArgumentError, 'Must specify name of a parameter') unless param and param.instance_of? String - - return '' if param.empty? - - if resource = findresource(reference.to_s) - return resource[param] if resource[param] - end - - return '' -end diff --git a/lib/puppet/parser/functions/getvar.rb b/lib/puppet/parser/functions/getvar.rb deleted file mode 100644 index fb336b6a..00000000 --- a/lib/puppet/parser/functions/getvar.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:getvar, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - Lookup a variable in a remote namespace. - - For example: - - $foo = getvar('site::data::foo') - # Equivalent to $foo = $site::data::foo - - This is useful if the namespace itself is stored in a string: - - $datalocation = 'site::data' - $bar = getvar("${datalocation}::bar") - # Equivalent to $bar = $site::data::bar - ENDHEREDOC - - unless args.length == 1 - raise Puppet::ParseError, ("getvar(): wrong number of arguments (#{args.length}; must be 1)") - end - - begin - self.lookupvar("#{args[0]}") - rescue Puppet::ParseError # Eat the exception if strict_variables = true is set - end - - end - -end diff --git a/lib/puppet/parser/functions/grep.rb b/lib/puppet/parser/functions/grep.rb deleted file mode 100644 index ceba9ecc..00000000 --- a/lib/puppet/parser/functions/grep.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# grep.rb -# - -module Puppet::Parser::Functions - newfunction(:grep, :type => :rvalue, :doc => <<-EOS -This function searches through an array and returns any elements that match -the provided regular expression. - -*Examples:* - - grep(['aaa','bbb','ccc','aaaddd'], 'aaa') - -Would return: - - ['aaa','aaaddd'] - EOS - ) do |arguments| - - if (arguments.size != 2) then - raise(Puppet::ParseError, "grep(): Wrong number of arguments "+ - "given #{arguments.size} for 2") - end - - a = arguments[0] - pattern = Regexp.new(arguments[1]) - - a.grep(pattern) - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/has_interface_with.rb b/lib/puppet/parser/functions/has_interface_with.rb deleted file mode 100644 index 36915246..00000000 --- a/lib/puppet/parser/functions/has_interface_with.rb +++ /dev/null @@ -1,68 +0,0 @@ -# -# has_interface_with -# - -module Puppet::Parser::Functions - newfunction(:has_interface_with, :type => :rvalue, :doc => <<-EOS -Returns boolean based on kind and value: - * macaddress - * netmask - * ipaddress - * network - -has_interface_with("macaddress", "x:x:x:x:x:x") -has_interface_with("ipaddress", "127.0.0.1") => true -etc. - -If no "kind" is given, then the presence of the interface is checked: -has_interface_with("lo") => true - EOS - ) do |args| - - raise(Puppet::ParseError, "has_interface_with(): Wrong number of arguments " + - "given (#{args.size} for 1 or 2)") if args.size < 1 or args.size > 2 - - interfaces = lookupvar('interfaces') - - # If we do not have any interfaces, then there are no requested attributes - return false if (interfaces == :undefined || interfaces.nil?) - - interfaces = interfaces.split(',') - - if args.size == 1 - return interfaces.member?(args[0]) - end - - kind, value = args - - # Bug with 3.7.1 - 3.7.3 when using future parser throws :undefined_variable - # https://tickets.puppetlabs.com/browse/PUP-3597 - factval = nil - catch :undefined_variable do - factval = lookupvar(kind) - end - if factval == value - return true - end - - result = false - interfaces.each do |iface| - iface.downcase! - factval = nil - begin - # Bug with 3.7.1 - 3.7.3 when using future parser throws :undefined_variable - # https://tickets.puppetlabs.com/browse/PUP-3597 - catch :undefined_variable do - factval = lookupvar("#{kind}_#{iface}") - end - rescue Puppet::ParseError # Eat the exception if strict_variables = true is set - end - if value == factval - result = true - break - end - end - - result - end -end diff --git a/lib/puppet/parser/functions/has_ip_address.rb b/lib/puppet/parser/functions/has_ip_address.rb deleted file mode 100644 index 842c8ec6..00000000 --- a/lib/puppet/parser/functions/has_ip_address.rb +++ /dev/null @@ -1,25 +0,0 @@ -# -# has_ip_address -# - -module Puppet::Parser::Functions - newfunction(:has_ip_address, :type => :rvalue, :doc => <<-EOS -Returns true if the client has the requested IP address on some interface. - -This function iterates through the 'interfaces' fact and checks the -'ipaddress_IFACE' facts, performing a simple string comparison. - EOS - ) do |args| - - raise(Puppet::ParseError, "has_ip_address(): Wrong number of arguments " + - "given (#{args.size} for 1)") if args.size != 1 - - Puppet::Parser::Functions.autoloader.load(:has_interface_with) \ - unless Puppet::Parser::Functions.autoloader.loaded?(:has_interface_with) - - function_has_interface_with(['ipaddress', args[0]]) - - end -end - -# vim:sts=2 sw=2 diff --git a/lib/puppet/parser/functions/has_ip_network.rb b/lib/puppet/parser/functions/has_ip_network.rb deleted file mode 100644 index 9ccf9024..00000000 --- a/lib/puppet/parser/functions/has_ip_network.rb +++ /dev/null @@ -1,25 +0,0 @@ -# -# has_ip_network -# - -module Puppet::Parser::Functions - newfunction(:has_ip_network, :type => :rvalue, :doc => <<-EOS -Returns true if the client has an IP address within the requested network. - -This function iterates through the 'interfaces' fact and checks the -'network_IFACE' facts, performing a simple string comparision. - EOS - ) do |args| - - raise(Puppet::ParseError, "has_ip_network(): Wrong number of arguments " + - "given (#{args.size} for 1)") if args.size != 1 - - Puppet::Parser::Functions.autoloader.load(:has_interface_with) \ - unless Puppet::Parser::Functions.autoloader.loaded?(:has_interface_with) - - function_has_interface_with(['network', args[0]]) - - end -end - -# vim:sts=2 sw=2 diff --git a/lib/puppet/parser/functions/has_key.rb b/lib/puppet/parser/functions/has_key.rb deleted file mode 100644 index 4657cc29..00000000 --- a/lib/puppet/parser/functions/has_key.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:has_key, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - Determine if a hash has a certain key value. - - Example: - - $my_hash = {'key_one' => 'value_one'} - if has_key($my_hash, 'key_two') { - notice('we will not reach here') - } - if has_key($my_hash, 'key_one') { - notice('this will be printed') - } - - ENDHEREDOC - - unless args.length == 2 - raise Puppet::ParseError, ("has_key(): wrong number of arguments (#{args.length}; must be 2)") - end - unless args[0].is_a?(Hash) - raise Puppet::ParseError, "has_key(): expects the first argument to be a hash, got #{args[0].inspect} which is of type #{args[0].class}" - end - args[0].has_key?(args[1]) - - end - -end diff --git a/lib/puppet/parser/functions/hash.rb b/lib/puppet/parser/functions/hash.rb deleted file mode 100644 index 8cc4823b..00000000 --- a/lib/puppet/parser/functions/hash.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# hash.rb -# - -module Puppet::Parser::Functions - newfunction(:hash, :type => :rvalue, :doc => <<-EOS -This function converts an array into a hash. - -*Examples:* - - hash(['a',1,'b',2,'c',3]) - -Would return: {'a'=>1,'b'=>2,'c'=>3} - EOS - ) do |arguments| - - raise(Puppet::ParseError, "hash(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - array = arguments[0] - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'hash(): Requires array to work with') - end - - result = {} - - begin - # This is to make it compatible with older version of Ruby ... - array = array.flatten - result = Hash[*array] - rescue Exception - raise(Puppet::ParseError, 'hash(): Unable to compute ' + - 'hash from array given') - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/intersection.rb b/lib/puppet/parser/functions/intersection.rb deleted file mode 100644 index 48f02e9d..00000000 --- a/lib/puppet/parser/functions/intersection.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# intersection.rb -# - -module Puppet::Parser::Functions - newfunction(:intersection, :type => :rvalue, :doc => <<-EOS -This function returns an array an intersection of two. - -*Examples:* - - intersection(["a","b","c"],["b","c","d"]) - -Would return: ["b","c"] - EOS - ) do |arguments| - - # Two arguments are required - raise(Puppet::ParseError, "intersection(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size != 2 - - first = arguments[0] - second = arguments[1] - - unless first.is_a?(Array) && second.is_a?(Array) - raise(Puppet::ParseError, 'intersection(): Requires 2 arrays') - end - - result = first & second - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_array.rb b/lib/puppet/parser/functions/is_array.rb deleted file mode 100644 index b39e184a..00000000 --- a/lib/puppet/parser/functions/is_array.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# is_array.rb -# - -module Puppet::Parser::Functions - newfunction(:is_array, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is an array. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "is_array(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - type = arguments[0] - - result = type.is_a?(Array) - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_bool.rb b/lib/puppet/parser/functions/is_bool.rb deleted file mode 100644 index 8bbdbc8a..00000000 --- a/lib/puppet/parser/functions/is_bool.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# is_bool.rb -# - -module Puppet::Parser::Functions - newfunction(:is_bool, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is a boolean. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "is_bool(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - type = arguments[0] - - result = type.is_a?(TrueClass) || type.is_a?(FalseClass) - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_domain_name.rb b/lib/puppet/parser/functions/is_domain_name.rb deleted file mode 100644 index b3fee965..00000000 --- a/lib/puppet/parser/functions/is_domain_name.rb +++ /dev/null @@ -1,50 +0,0 @@ -# -# is_domain_name.rb -# - -module Puppet::Parser::Functions - newfunction(:is_domain_name, :type => :rvalue, :doc => <<-EOS -Returns true if the string passed to this function is a syntactically correct domain name. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_domain_name(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - domain = arguments[0] - - # Limits (rfc1035, 3.1) - domain_max_length=255 - label_min_length=1 - label_max_length=63 - - # Only allow string types - return false unless domain.is_a?(String) - - # Allow ".", it is the top level domain - return true if domain == '.' - - # Remove the final dot, if present. - domain.chomp!('.') - - # Check the whole domain - return false if domain.empty? - return false if domain.length > domain_max_length - - # Check each label in the domain - labels = domain.split('.') - vlabels = labels.each do |label| - break if label.length < label_min_length - break if label.length > label_max_length - break if label[-1..-1] == '-' - break if label[0..0] == '-' - break unless /^[a-z\d-]+$/i.match(label) - end - return vlabels == labels - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_float.rb b/lib/puppet/parser/functions/is_float.rb deleted file mode 100644 index a2da9438..00000000 --- a/lib/puppet/parser/functions/is_float.rb +++ /dev/null @@ -1,30 +0,0 @@ -# -# is_float.rb -# - -module Puppet::Parser::Functions - newfunction(:is_float, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is a float. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_float(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - value = arguments[0] - - # Only allow Numeric or String types - return false unless value.is_a?(Numeric) or value.is_a?(String) - - if value != value.to_f.to_s and !value.is_a? Float then - return false - else - return true - end - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_function_available.rb b/lib/puppet/parser/functions/is_function_available.rb deleted file mode 100644 index 6da82c8c..00000000 --- a/lib/puppet/parser/functions/is_function_available.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# is_function_available.rb -# - -module Puppet::Parser::Functions - newfunction(:is_function_available, :type => :rvalue, :doc => <<-EOS -This function accepts a string as an argument, determines whether the -Puppet runtime has access to a function by that name. It returns a -true if the function exists, false if not. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_function_available?(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - # Only allow String types - return false unless arguments[0].is_a?(String) - - function = Puppet::Parser::Functions.function(arguments[0].to_sym) - function.is_a?(String) and not function.empty? - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_hash.rb b/lib/puppet/parser/functions/is_hash.rb deleted file mode 100644 index ad907f08..00000000 --- a/lib/puppet/parser/functions/is_hash.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# is_hash.rb -# - -module Puppet::Parser::Functions - newfunction(:is_hash, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is a hash. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "is_hash(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - type = arguments[0] - - result = type.is_a?(Hash) - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_integer.rb b/lib/puppet/parser/functions/is_integer.rb deleted file mode 100644 index c03d28df..00000000 --- a/lib/puppet/parser/functions/is_integer.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# is_integer.rb -# - -module Puppet::Parser::Functions - newfunction(:is_integer, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is an Integer or -a decimal (base 10) integer in String form. The string may -start with a '-' (minus). A value of '0' is allowed, but a leading '0' digit may not -be followed by other digits as this indicates that the value is octal (base 8). - -If given any other argument `false` is returned. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_integer(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - value = arguments[0] - - # Regex is taken from the lexer of puppet - # puppet/pops/parser/lexer.rb but modified to match also - # negative values and disallow numbers prefixed with multiple - # 0's - # - # TODO these parameter should be a constant but I'm not sure - # if there is no risk to declare it inside of the module - # Puppet::Parser::Functions - - # Integer numbers like - # -1234568981273 - # 47291 - numeric = %r{^-?(?:(?:[1-9]\d*)|0)$} - - if value.is_a? Integer or (value.is_a? String and value.match numeric) - return true - else - return false - end - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_ip_address.rb b/lib/puppet/parser/functions/is_ip_address.rb deleted file mode 100644 index a90adabe..00000000 --- a/lib/puppet/parser/functions/is_ip_address.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# is_ip_address.rb -# - -module Puppet::Parser::Functions - newfunction(:is_ip_address, :type => :rvalue, :doc => <<-EOS -Returns true if the string passed to this function is a valid IP address. - EOS - ) do |arguments| - - require 'ipaddr' - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_ip_address(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - begin - ip = IPAddr.new(arguments[0]) - rescue ArgumentError - return false - end - - if ip.ipv4? or ip.ipv6? then - return true - else - return false - end - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_mac_address.rb b/lib/puppet/parser/functions/is_mac_address.rb deleted file mode 100644 index 1b3088a2..00000000 --- a/lib/puppet/parser/functions/is_mac_address.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# is_mac_address.rb -# - -module Puppet::Parser::Functions - newfunction(:is_mac_address, :type => :rvalue, :doc => <<-EOS -Returns true if the string passed to this function is a valid mac address. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_mac_address(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - mac = arguments[0] - - if /^[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}$/.match(mac) then - return true - else - return false - end - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_numeric.rb b/lib/puppet/parser/functions/is_numeric.rb deleted file mode 100644 index e7e1d2a7..00000000 --- a/lib/puppet/parser/functions/is_numeric.rb +++ /dev/null @@ -1,75 +0,0 @@ -# -# is_numeric.rb -# - -module Puppet::Parser::Functions - newfunction(:is_numeric, :type => :rvalue, :doc => <<-EOS -Returns true if the given argument is a Numeric (Integer or Float), -or a String containing either a valid integer in decimal base 10 form, or -a valid floating point string representation. - -The function recognizes only decimal (base 10) integers and float but not -integers in hex (base 16) or octal (base 8) form. - -The string representation may start with a '-' (minus). If a decimal '.' is used, -it must be followed by at least one digit. - -Valid examples: - - 77435 - 10e-12 - -8475 - 0.2343 - -23.561e3 - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "is_numeric(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - value = arguments[0] - - # Regex is taken from the lexer of puppet - # puppet/pops/parser/lexer.rb but modified to match also - # negative values and disallow invalid octal numbers or - # numbers prefixed with multiple 0's (except in hex numbers) - # - # TODO these parameters should be constants but I'm not sure - # if there is no risk to declare them inside of the module - # Puppet::Parser::Functions - - # TODO decide if this should be used - # HEX numbers like - # 0xaa230F - # 0X1234009C - # 0x0012 - # -12FcD - #numeric_hex = %r{^-?0[xX][0-9A-Fa-f]+$} - - # TODO decide if this should be used - # OCTAL numbers like - # 01234567 - # -045372 - #numeric_oct = %r{^-?0[1-7][0-7]*$} - - # Integer/Float numbers like - # -0.1234568981273 - # 47291 - # 42.12345e-12 - numeric = %r{^-?(?:(?:[1-9]\d*)|0)(?:\.\d+)?(?:[eE]-?\d+)?$} - - if value.is_a? Numeric or (value.is_a? String and ( - value.match(numeric) #or - # value.match(numeric_hex) or - # value.match(numeric_oct) - )) - return true - else - return false - end - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/is_string.rb b/lib/puppet/parser/functions/is_string.rb deleted file mode 100644 index f5bef045..00000000 --- a/lib/puppet/parser/functions/is_string.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# is_string.rb -# - -module Puppet::Parser::Functions - newfunction(:is_string, :type => :rvalue, :doc => <<-EOS -Returns true if the variable passed to this function is a string. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "is_string(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - type = arguments[0] - - result = type.is_a?(String) - - if result and (type == type.to_f.to_s or type == type.to_i.to_s) then - return false - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/join.rb b/lib/puppet/parser/functions/join.rb deleted file mode 100644 index 6c0a6ba0..00000000 --- a/lib/puppet/parser/functions/join.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# join.rb -# - -module Puppet::Parser::Functions - newfunction(:join, :type => :rvalue, :doc => <<-EOS -This function joins an array into a string using a separator. - -*Examples:* - - join(['a','b','c'], ",") - -Would result in: "a,b,c" - EOS - ) do |arguments| - - # Technically we support two arguments but only first is mandatory ... - raise(Puppet::ParseError, "join(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - array = arguments[0] - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'join(): Requires array to work with') - end - - suffix = arguments[1] if arguments[1] - - if suffix - unless suffix.is_a?(String) - raise(Puppet::ParseError, 'join(): Requires string to work with') - end - end - - result = suffix ? array.join(suffix) : array.join - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/join_keys_to_values.rb b/lib/puppet/parser/functions/join_keys_to_values.rb deleted file mode 100644 index e9924fe2..00000000 --- a/lib/puppet/parser/functions/join_keys_to_values.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# join.rb -# - -module Puppet::Parser::Functions - newfunction(:join_keys_to_values, :type => :rvalue, :doc => <<-EOS -This function joins each key of a hash to that key's corresponding value with a -separator. Keys and values are cast to strings. The return value is an array in -which each element is one joined key/value pair. - -*Examples:* - - join_keys_to_values({'a'=>1,'b'=>2}, " is ") - -Would result in: ["a is 1","b is 2"] - EOS - ) do |arguments| - - # Validate the number of arguments. - if arguments.size != 2 - raise(Puppet::ParseError, "join_keys_to_values(): Takes exactly two " + - "arguments, but #{arguments.size} given.") - end - - # Validate the first argument. - hash = arguments[0] - if not hash.is_a?(Hash) - raise(TypeError, "join_keys_to_values(): The first argument must be a " + - "hash, but a #{hash.class} was given.") - end - - # Validate the second argument. - separator = arguments[1] - if not separator.is_a?(String) - raise(TypeError, "join_keys_to_values(): The second argument must be a " + - "string, but a #{separator.class} was given.") - end - - # Join the keys to their values. - hash.map do |k,v| - String(k) + separator + String(v) - end - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/keys.rb b/lib/puppet/parser/functions/keys.rb deleted file mode 100644 index f0d13b64..00000000 --- a/lib/puppet/parser/functions/keys.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# keys.rb -# - -module Puppet::Parser::Functions - newfunction(:keys, :type => :rvalue, :doc => <<-EOS -Returns the keys of a hash as an array. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "keys(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - hash = arguments[0] - - unless hash.is_a?(Hash) - raise(Puppet::ParseError, 'keys(): Requires hash to work with') - end - - result = hash.keys - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/loadyaml.rb b/lib/puppet/parser/functions/loadyaml.rb deleted file mode 100644 index 10c40050..00000000 --- a/lib/puppet/parser/functions/loadyaml.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:loadyaml, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - Load a YAML file containing an array, string, or hash, and return the data - in the corresponding native data type. - - For example: - - $myhash = loadyaml('/etc/puppet/data/myhash.yaml') - ENDHEREDOC - - unless args.length == 1 - raise Puppet::ParseError, ("loadyaml(): wrong number of arguments (#{args.length}; must be 1)") - end - - YAML.load_file(args[0]) - - end - -end diff --git a/lib/puppet/parser/functions/lstrip.rb b/lib/puppet/parser/functions/lstrip.rb deleted file mode 100644 index 624e4c84..00000000 --- a/lib/puppet/parser/functions/lstrip.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# lstrip.rb -# - -module Puppet::Parser::Functions - newfunction(:lstrip, :type => :rvalue, :doc => <<-EOS -Strips leading spaces to the left of a string. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "lstrip(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'lstrip(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.lstrip : i } - else - result = value.lstrip - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/max.rb b/lib/puppet/parser/functions/max.rb deleted file mode 100644 index 60fb94ac..00000000 --- a/lib/puppet/parser/functions/max.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:max, :type => :rvalue, :doc => <<-EOS - Returns the highest value of all arguments. - Requires at least one argument. - EOS - ) do |args| - - raise(Puppet::ParseError, "max(): Wrong number of arguments " + - "need at least one") if args.size == 0 - - # Sometimes we get numbers as numerics and sometimes as strings. - # We try to compare them as numbers when possible - return args.max do |a,b| - if a.to_s =~ /\A-?\d+(.\d+)?\z/ and b.to_s =~ /\A-?\d+(.\d+)?\z/ then - a.to_f <=> b.to_f - else - a.to_s <=> b.to_s - end - end - end -end diff --git a/lib/puppet/parser/functions/member.rb b/lib/puppet/parser/functions/member.rb deleted file mode 100644 index 88609ce5..00000000 --- a/lib/puppet/parser/functions/member.rb +++ /dev/null @@ -1,62 +0,0 @@ -# -# member.rb -# - -# TODO(Krzysztof Wilczynski): We need to add support for regular expression ... -# TODO(Krzysztof Wilczynski): Support for strings and hashes too ... - -module Puppet::Parser::Functions - newfunction(:member, :type => :rvalue, :doc => <<-EOS -This function determines if a variable is a member of an array. -The variable can be a string, fixnum, or array. - -*Examples:* - - member(['a','b'], 'b') - -Would return: true - - member(['a', 'b', 'c'], ['a', 'b']) - -would return: true - - member(['a','b'], 'c') - -Would return: false - - member(['a', 'b', 'c'], ['d', 'b']) - -would return: false - EOS - ) do |arguments| - - raise(Puppet::ParseError, "member(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size < 2 - - array = arguments[0] - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'member(): Requires array to work with') - end - - unless arguments[1].is_a? String or arguments[1].is_a? Fixnum or arguments[1].is_a? Array - raise(Puppet::ParseError, 'member(): Item to search for must be a string, fixnum, or array') - end - - if arguments[1].is_a? String or arguments[1].is_a? Fixnum - item = Array(arguments[1]) - else - item = arguments[1] - end - - - raise(Puppet::ParseError, 'member(): You must provide item ' + - 'to search for within array given') if item.respond_to?('empty?') && item.empty? - - result = (item - array).empty? - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/merge.rb b/lib/puppet/parser/functions/merge.rb deleted file mode 100644 index 1b39f206..00000000 --- a/lib/puppet/parser/functions/merge.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:merge, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| - Merges two or more hashes together and returns the resulting hash. - - For example: - - $hash1 = {'one' => 1, 'two', => 2} - $hash2 = {'two' => 'dos', 'three', => 'tres'} - $merged_hash = merge($hash1, $hash2) - # The resulting hash is equivalent to: - # $merged_hash = {'one' => 1, 'two' => 'dos', 'three' => 'tres'} - - When there is a duplicate key, the key in the rightmost hash will "win." - - ENDHEREDOC - - if args.length < 2 - raise Puppet::ParseError, ("merge(): wrong number of arguments (#{args.length}; must be at least 2)") - end - - # The hash we accumulate into - accumulator = Hash.new - # Merge into the accumulator hash - args.each do |arg| - next if arg.is_a? String and arg.empty? # empty string is synonym for puppet's undef - unless arg.is_a?(Hash) - raise Puppet::ParseError, "merge: unexpected argument type #{arg.class}, only expects hash arguments" - end - accumulator.merge!(arg) - end - # Return the fully merged hash - accumulator - end -end diff --git a/lib/puppet/parser/functions/min.rb b/lib/puppet/parser/functions/min.rb deleted file mode 100644 index 6bd6ebf2..00000000 --- a/lib/puppet/parser/functions/min.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:min, :type => :rvalue, :doc => <<-EOS - Returns the lowest value of all arguments. - Requires at least one argument. - EOS - ) do |args| - - raise(Puppet::ParseError, "min(): Wrong number of arguments " + - "need at least one") if args.size == 0 - - # Sometimes we get numbers as numerics and sometimes as strings. - # We try to compare them as numbers when possible - return args.min do |a,b| - if a.to_s =~ /\A^-?\d+(.\d+)?\z/ and b.to_s =~ /\A-?\d+(.\d+)?\z/ then - a.to_f <=> b.to_f - else - a.to_s <=> b.to_s - end - end - end -end diff --git a/lib/puppet/parser/functions/num2bool.rb b/lib/puppet/parser/functions/num2bool.rb deleted file mode 100644 index af0e6ed7..00000000 --- a/lib/puppet/parser/functions/num2bool.rb +++ /dev/null @@ -1,43 +0,0 @@ -# -# num2bool.rb -# - -module Puppet::Parser::Functions - newfunction(:num2bool, :type => :rvalue, :doc => <<-EOS -This function converts a number or a string representation of a number into a -true boolean. Zero or anything non-numeric becomes false. Numbers higher then 0 -become true. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "num2bool(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - number = arguments[0] - - case number - when Numeric - # Yay, it's a number - when String - begin - number = Float(number) - rescue ArgumentError => ex - raise(Puppet::ParseError, "num2bool(): '#{number}' does not look like a number: #{ex.message}") - end - else - begin - number = number.to_s - rescue NoMethodError => ex - raise(Puppet::ParseError, "num2bool(): Unable to parse argument: #{ex.message}") - end - end - - # Truncate Floats - number = number.to_i - - # Return true for any positive number and false otherwise - return number > 0 - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/obfuscate_email.rb b/lib/puppet/parser/functions/obfuscate_email.rb deleted file mode 100644 index 4e4cb826..00000000 --- a/lib/puppet/parser/functions/obfuscate_email.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:obfuscate_email, :type => :rvalue, :doc => <<-EOS -Given: - a comma seperated email string in form of 'john@doe.com, doe@john.com' - -This function will return all emails obfuscated in form of 'john {at} doe {dot} com, doe {at} john {dot} com' -Works with multiple email adresses as well as with a single email adress. - - EOS - ) do |args| - args[0].gsub('@', ' {at} ').gsub('.', ' {dot} ') - end -end - -# vim: set ts=2 sw=2 et : -# encoding: utf-8 diff --git a/lib/puppet/parser/functions/parsejson.rb b/lib/puppet/parser/functions/parsejson.rb deleted file mode 100644 index a9a16a45..00000000 --- a/lib/puppet/parser/functions/parsejson.rb +++ /dev/null @@ -1,24 +0,0 @@ -# -# parsejson.rb -# - -module Puppet::Parser::Functions - newfunction(:parsejson, :type => :rvalue, :doc => <<-EOS -This function accepts JSON as a string and converts into the correct Puppet -structure. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "parsejson(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - json = arguments[0] - - # PSON is natively available in puppet - PSON.load(json) - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/parseyaml.rb b/lib/puppet/parser/functions/parseyaml.rb deleted file mode 100644 index 53d54faf..00000000 --- a/lib/puppet/parser/functions/parseyaml.rb +++ /dev/null @@ -1,24 +0,0 @@ -# -# parseyaml.rb -# - -module Puppet::Parser::Functions - newfunction(:parseyaml, :type => :rvalue, :doc => <<-EOS -This function accepts YAML as a string and converts it into the correct -Puppet structure. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "parseyaml(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - require 'yaml' - - YAML::load(arguments[0]) - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/pick.rb b/lib/puppet/parser/functions/pick.rb deleted file mode 100644 index fdd0aefd..00000000 --- a/lib/puppet/parser/functions/pick.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:pick, :type => :rvalue, :doc => <<-EOS - -This function is similar to a coalesce function in SQL in that it will return -the first value in a list of values that is not undefined or an empty string -(two things in Puppet that will return a boolean false value). Typically, -this function is used to check for a value in the Puppet Dashboard/Enterprise -Console, and failover to a default value like the following: - - $real_jenkins_version = pick($::jenkins_version, '1.449') - -The value of $real_jenkins_version will first look for a top-scope variable -called 'jenkins_version' (note that parameters set in the Puppet Dashboard/ -Enterprise Console are brought into Puppet as top-scope variables), and, -failing that, will use a default value of 1.449. - -EOS -) do |args| - args = args.compact - args.delete(:undef) - args.delete(:undefined) - args.delete("") - if args[0].to_s.empty? then - fail Puppet::ParseError, "pick(): must receive at least one non empty value" - else - return args[0] - end - end -end diff --git a/lib/puppet/parser/functions/pick_default.rb b/lib/puppet/parser/functions/pick_default.rb deleted file mode 100644 index 36e33abf..00000000 --- a/lib/puppet/parser/functions/pick_default.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:pick_default, :type => :rvalue, :doc => <<-EOS - -This function is similar to a coalesce function in SQL in that it will return -the first value in a list of values that is not undefined or an empty string -(two things in Puppet that will return a boolean false value). If no value is -found, it will return the last argument. - -Typically, this function is used to check for a value in the Puppet -Dashboard/Enterprise Console, and failover to a default value like the -following: - - $real_jenkins_version = pick_default($::jenkins_version, '1.449') - -The value of $real_jenkins_version will first look for a top-scope variable -called 'jenkins_version' (note that parameters set in the Puppet Dashboard/ -Enterprise Console are brought into Puppet as top-scope variables), and, -failing that, will use a default value of 1.449. - -Note that, contrary to the pick() function, the pick_default does not fail if -all arguments are empty. This allows pick_default to use an empty value as -default. - -EOS -) do |args| - fail "Must receive at least one argument." if args.empty? - default = args.last - args = args[0..-2].compact - args.delete(:undef) - args.delete(:undefined) - args.delete("") - args << default - return args[0] - end -end diff --git a/lib/puppet/parser/functions/prefix.rb b/lib/puppet/parser/functions/prefix.rb deleted file mode 100644 index d02286af..00000000 --- a/lib/puppet/parser/functions/prefix.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# prefix.rb -# - -module Puppet::Parser::Functions - newfunction(:prefix, :type => :rvalue, :doc => <<-EOS -This function applies a prefix to all elements in an array. - -*Examples:* - - prefix(['a','b','c'], 'p') - -Will return: ['pa','pb','pc'] - EOS - ) do |arguments| - - # Technically we support two arguments but only first is mandatory ... - raise(Puppet::ParseError, "prefix(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - array = arguments[0] - - unless array.is_a?(Array) - raise Puppet::ParseError, "prefix(): expected first argument to be an Array, got #{array.inspect}" - end - - prefix = arguments[1] if arguments[1] - - if prefix - unless prefix.is_a?(String) - raise Puppet::ParseError, "prefix(): expected second argument to be a String, got #{prefix.inspect}" - end - end - - # Turn everything into string same as join would do ... - result = array.collect do |i| - i = i.to_s - prefix ? prefix + i : i - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/private.rb b/lib/puppet/parser/functions/private.rb deleted file mode 100644 index 60210d33..00000000 --- a/lib/puppet/parser/functions/private.rb +++ /dev/null @@ -1,29 +0,0 @@ -# -# private.rb -# - -module Puppet::Parser::Functions - newfunction(:private, :doc => <<-'EOS' - Sets the current class or definition as private. - Calling the class or definition from outside the current module will fail. - EOS - ) do |args| - - raise(Puppet::ParseError, "private(): Wrong number of arguments "+ - "given (#{args.size}}) for 0 or 1)") if args.size > 1 - - scope = self - if scope.lookupvar('module_name') != scope.lookupvar('caller_module_name') - message = nil - if args[0] and args[0].is_a? String - message = args[0] - else - manifest_name = scope.source.name - manifest_type = scope.source.type - message = (manifest_type.to_s == 'hostclass') ? 'Class' : 'Definition' - message += " #{manifest_name} is private" - end - raise(Puppet::ParseError, message) - end - end -end diff --git a/lib/puppet/parser/functions/range.rb b/lib/puppet/parser/functions/range.rb deleted file mode 100644 index 49fba21c..00000000 --- a/lib/puppet/parser/functions/range.rb +++ /dev/null @@ -1,88 +0,0 @@ -# -# range.rb -# - -# TODO(Krzysztof Wilczynski): We probably need to approach numeric values differently ... - -module Puppet::Parser::Functions - newfunction(:range, :type => :rvalue, :doc => <<-EOS -When given range in the form of (start, stop) it will extrapolate a range as -an array. - -*Examples:* - - range("0", "9") - -Will return: [0,1,2,3,4,5,6,7,8,9] - - range("00", "09") - -Will return: [0,1,2,3,4,5,6,7,8,9] (Zero padded strings are converted to -integers automatically) - - range("a", "c") - -Will return: ["a","b","c"] - - range("host01", "host10") - -Will return: ["host01", "host02", ..., "host09", "host10"] - -Passing a third argument will cause the generated range to step by that -interval, e.g. - - range("0", "9", "2") - -Will return: [0,2,4,6,8] - EOS - ) do |arguments| - - # We support more than one argument but at least one is mandatory ... - raise(Puppet::ParseError, "range(): Wrong number of " + - "arguments given (#{arguments.size} for 1)") if arguments.size < 1 - - if arguments.size > 1 - start = arguments[0] - stop = arguments[1] - step = arguments[2].nil? ? 1 : arguments[2].to_i.abs - - type = '..' # We select simplest type for Range available in Ruby ... - - elsif arguments.size > 0 - value = arguments[0] - - if m = value.match(/^(\w+)(\.\.\.?|\-)(\w+)$/) - start = m[1] - stop = m[3] - - type = m[2] - - elsif value.match(/^.+$/) - raise(Puppet::ParseError, 'range(): Unable to compute range ' + - 'from the value given') - else - raise(Puppet::ParseError, 'range(): Unknown format of range given') - end - end - - # Check whether we have integer value if so then make it so ... - if start.to_s.match(/^\d+$/) - start = start.to_i - stop = stop.to_i - else - start = start.to_s - stop = stop.to_s - end - - range = case type - when /^(\.\.|\-)$/ then (start .. stop) - when /^(\.\.\.)$/ then (start ... stop) # Exclusive of last element ... - end - - result = range.step(step).collect { |i| i } # Get them all ... Pokemon ... - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/reject.rb b/lib/puppet/parser/functions/reject.rb deleted file mode 100644 index 1953ffcf..00000000 --- a/lib/puppet/parser/functions/reject.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# reject.rb -# - -module Puppet::Parser::Functions - newfunction(:reject, :type => :rvalue, :doc => <<-EOS) do |args| -This function searches through an array and rejects all elements that match -the provided regular expression. - -*Examples:* - - reject(['aaa','bbb','ccc','aaaddd'], 'aaa') - -Would return: - - ['bbb','ccc'] -EOS - - if (args.size != 2) - raise Puppet::ParseError, - "reject(): Wrong number of arguments given #{args.size} for 2" - end - - ary = args[0] - pattern = Regexp.new(args[1]) - - ary.reject { |e| e =~ pattern } - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/reverse.rb b/lib/puppet/parser/functions/reverse.rb deleted file mode 100644 index 7f1018f6..00000000 --- a/lib/puppet/parser/functions/reverse.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# reverse.rb -# - -module Puppet::Parser::Functions - newfunction(:reverse, :type => :rvalue, :doc => <<-EOS -Reverses the order of a string or array. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "reverse(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'reverse(): Requires either ' + - 'array or string to work with') - end - - result = value.reverse - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/rstrip.rb b/lib/puppet/parser/functions/rstrip.rb deleted file mode 100644 index 0cf8d222..00000000 --- a/lib/puppet/parser/functions/rstrip.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# rstrip.rb -# - -module Puppet::Parser::Functions - newfunction(:rstrip, :type => :rvalue, :doc => <<-EOS -Strips leading spaces to the right of the string. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "rstrip(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'rstrip(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - result = value.collect { |i| i.is_a?(String) ? i.rstrip : i } - else - result = value.rstrip - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/shuffle.rb b/lib/puppet/parser/functions/shuffle.rb deleted file mode 100644 index 30c663db..00000000 --- a/lib/puppet/parser/functions/shuffle.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# shuffle.rb -# - -module Puppet::Parser::Functions - newfunction(:shuffle, :type => :rvalue, :doc => <<-EOS -Randomizes the order of a string or array elements. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "shuffle(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'shuffle(): Requires either ' + - 'array or string to work with') - end - - result = value.clone - - string = value.is_a?(String) ? true : false - - # Check whether it makes sense to shuffle ... - return result if result.size <= 1 - - # We turn any string value into an array to be able to shuffle ... - result = string ? result.split('') : result - - elements = result.size - - # Simple implementation of Fisher–Yates in-place shuffle ... - elements.times do |i| - j = rand(elements - i) + i - result[j], result[i] = result[i], result[j] - end - - result = string ? result.join : result - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/size.rb b/lib/puppet/parser/functions/size.rb deleted file mode 100644 index cc207e3f..00000000 --- a/lib/puppet/parser/functions/size.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -# size.rb -# - -# TODO(Krzysztof Wilczynski): Support for hashes would be nice too ... - -module Puppet::Parser::Functions - newfunction(:size, :type => :rvalue, :doc => <<-EOS -Returns the number of elements in a string or array. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "size(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - item = arguments[0] - - if item.is_a?(String) - - begin - # - # Check whether your item is a numeric value or not ... - # This will take care about positive and/or negative numbers - # for both integer and floating-point values ... - # - # Please note that Puppet has no notion of hexadecimal - # nor octal numbers for its DSL at this point in time ... - # - Float(item) - - raise(Puppet::ParseError, 'size(): Requires either ' + - 'string or array to work with') - - rescue ArgumentError - result = item.size - end - - elsif item.is_a?(Array) - result = item.size - else - raise(Puppet::ParseError, 'size(): Unknown type given') - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/sort.rb b/lib/puppet/parser/functions/sort.rb deleted file mode 100644 index cefbe546..00000000 --- a/lib/puppet/parser/functions/sort.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# sort.rb -# - -module Puppet::Parser::Functions - newfunction(:sort, :type => :rvalue, :doc => <<-EOS -Sorts strings and arrays lexically. - EOS - ) do |arguments| - - if (arguments.size != 1) then - raise(Puppet::ParseError, "sort(): Wrong number of arguments "+ - "given #{arguments.size} for 1") - end - - value = arguments[0] - - if value.is_a?(Array) then - value.sort - elsif value.is_a?(String) then - value.split("").sort.join("") - end - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/squeeze.rb b/lib/puppet/parser/functions/squeeze.rb deleted file mode 100644 index 81fadfdb..00000000 --- a/lib/puppet/parser/functions/squeeze.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -# squeeze.rb -# - -module Puppet::Parser::Functions - newfunction(:squeeze, :type => :rvalue, :doc => <<-EOS -Returns a new string where runs of the same character that occur in this set are replaced by a single character. - EOS - ) do |arguments| - - if ((arguments.size != 2) and (arguments.size != 1)) then - raise(Puppet::ParseError, "squeeze(): Wrong number of arguments "+ - "given #{arguments.size} for 2 or 1") - end - - item = arguments[0] - squeezeval = arguments[1] - - if item.is_a?(Array) then - if squeezeval then - item.collect { |i| i.squeeze(squeezeval) } - else - item.collect { |i| i.squeeze } - end - else - if squeezeval then - item.squeeze(squeezeval) - else - item.squeeze - end - end - - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/str2bool.rb b/lib/puppet/parser/functions/str2bool.rb deleted file mode 100644 index 446732ec..00000000 --- a/lib/puppet/parser/functions/str2bool.rb +++ /dev/null @@ -1,46 +0,0 @@ -# -# str2bool.rb -# - -module Puppet::Parser::Functions - newfunction(:str2bool, :type => :rvalue, :doc => <<-EOS -This converts a string to a boolean. This attempt to convert strings that -contain things like: y, 1, t, true to 'true' and strings that contain things -like: 0, f, n, false, no to 'false'. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "str2bool(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - string = arguments[0] - - # If string is already Boolean, return it - if !!string == string - return string - end - - unless string.is_a?(String) - raise(Puppet::ParseError, 'str2bool(): Requires either ' + - 'string to work with') - end - - # We consider all the yes, no, y, n and so on too ... - result = case string - # - # This is how undef looks like in Puppet ... - # We yield false in this case. - # - when /^$/, '' then false # Empty string will be false ... - when /^(1|t|y|true|yes)$/ then true - when /^(0|f|n|false|no)$/ then false - when /^(undef|undefined)$/ then false # This is not likely to happen ... - else - raise(Puppet::ParseError, 'str2bool(): Unknown type of boolean given') - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/str2saltedsha1.rb b/lib/puppet/parser/functions/str2saltedsha1.rb deleted file mode 100644 index e51a861a..00000000 --- a/lib/puppet/parser/functions/str2saltedsha1.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# str2saltedsha1.rb -# - -module Puppet::Parser::Functions - newfunction(:str2saltedsha1, :type => :rvalue, :doc => <<-EOS -This converts a string to a salted-SHA1 password hash (which is used for -OS X versions >= 10.7). Given any simple string, you will get a hex version -of a salted-SHA1 password hash that can be inserted into your Puppet -manifests as a valid password attribute. - EOS - ) do |arguments| - require 'digest/sha2' - - raise(Puppet::ParseError, "str2saltedsha1(): Wrong number of arguments " + - "passed (#{arguments.size} but we require 1)") if arguments.size != 1 - - password = arguments[0] - - unless password.is_a?(String) - raise(Puppet::ParseError, 'str2saltedsha1(): Requires a ' + - "String argument, you passed: #{password.class}") - end - - seedint = rand(2**31 - 1) - seedstring = Array(seedint).pack("L") - saltedpass = Digest::SHA1.digest(seedstring + password) - (seedstring + saltedpass).unpack('H*')[0] - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/str2saltedsha512.rb b/lib/puppet/parser/functions/str2saltedsha512.rb deleted file mode 100644 index 7fe7b012..00000000 --- a/lib/puppet/parser/functions/str2saltedsha512.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# str2saltedsha512.rb -# - -module Puppet::Parser::Functions - newfunction(:str2saltedsha512, :type => :rvalue, :doc => <<-EOS -This converts a string to a salted-SHA512 password hash (which is used for -OS X versions >= 10.7). Given any simple string, you will get a hex version -of a salted-SHA512 password hash that can be inserted into your Puppet -manifests as a valid password attribute. - EOS - ) do |arguments| - require 'digest/sha2' - - raise(Puppet::ParseError, "str2saltedsha512(): Wrong number of arguments " + - "passed (#{arguments.size} but we require 1)") if arguments.size != 1 - - password = arguments[0] - - unless password.is_a?(String) - raise(Puppet::ParseError, 'str2saltedsha512(): Requires a ' + - "String argument, you passed: #{password.class}") - end - - seedint = rand(2**31 - 1) - seedstring = Array(seedint).pack("L") - saltedpass = Digest::SHA512.digest(seedstring + password) - (seedstring + saltedpass).unpack('H*')[0] - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/str2sha1_and_salt.rb b/lib/puppet/parser/functions/str2sha1_and_salt.rb deleted file mode 100644 index 9ec382d0..00000000 --- a/lib/puppet/parser/functions/str2sha1_and_salt.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -# str2saltedsha1.rb -# - -module Puppet::Parser::Functions - newfunction(:str2sha1_and_salt, :type => :rvalue, :doc => <<-EOS -This converts a string to an array containing the salted SHA1 password hash in -the first field, and the salt itself in second field of the returned array. -This combination is used i.e. for couchdb passwords. - EOS - ) do |arguments| - require 'digest/sha1' - - raise(Puppet::ParseError, "str2saltedsha1(): Wrong number of arguments " + - "passed (#{arguments.size} but we require 1)") if arguments.size != 1 - - password = arguments[0] - - unless password.is_a?(String) - raise(Puppet::ParseError, 'str2saltedsha1(): Requires a ' + - "String argument, you passed: #{password.class}") - end - - seedint = rand(2**31 - 1) - seedstring = Array(seedint).pack("L") - salt = Digest::MD5.hexdigest(seedstring) - saltedpass = Digest::SHA1.hexdigest(password + salt) - - array = Array.new - array << saltedpass - array << salt - return array - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/str_and_salt2sha1.rb b/lib/puppet/parser/functions/str_and_salt2sha1.rb deleted file mode 100644 index 71d69cf5..00000000 --- a/lib/puppet/parser/functions/str_and_salt2sha1.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# str_and_salt2sha1.rb -# - -module Puppet::Parser::Functions - newfunction(:str_and_salt2sha1, :type => :rvalue, :doc => <<-EOS -This converts a string to an array containing the salted SHA1 password hash in -the first field, and the salt itself in second field of the returned array. -This combination is used i.e. for couchdb passwords. - EOS - ) do |arguments| - require 'digest/sha1' - - raise(Puppet::ParseError, "str_and_salt2sha1(): Wrong number of arguments " + - "passed (#{arguments.size} but we require 1)") if arguments.size != 1 - - str_and_salt = arguments[0] - - unless str_and_salt.is_a?(Array) - raise(Puppet::ParseError, 'str_and_salt2sha1(): Requires a ' + - "Array argument, you passed: #{password.class}") - end - - str = str_and_salt[0] - salt = str_and_salt[1] - sha1 = Digest::SHA1.hexdigest(str+ salt) - - return sha1 - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/strftime.rb b/lib/puppet/parser/functions/strftime.rb deleted file mode 100644 index 0b52adec..00000000 --- a/lib/puppet/parser/functions/strftime.rb +++ /dev/null @@ -1,107 +0,0 @@ -# -# strftime.rb -# - -module Puppet::Parser::Functions - newfunction(:strftime, :type => :rvalue, :doc => <<-EOS -This function returns formatted time. - -*Examples:* - -To return the time since epoch: - - strftime("%s") - -To return the date: - - strftime("%Y-%m-%d") - -*Format meaning:* - - %a - The abbreviated weekday name (``Sun'') - %A - The full weekday name (``Sunday'') - %b - The abbreviated month name (``Jan'') - %B - The full month name (``January'') - %c - The preferred local date and time representation - %C - Century (20 in 2009) - %d - Day of the month (01..31) - %D - Date (%m/%d/%y) - %e - Day of the month, blank-padded ( 1..31) - %F - Equivalent to %Y-%m-%d (the ISO 8601 date format) - %h - Equivalent to %b - %H - Hour of the day, 24-hour clock (00..23) - %I - Hour of the day, 12-hour clock (01..12) - %j - Day of the year (001..366) - %k - hour, 24-hour clock, blank-padded ( 0..23) - %l - hour, 12-hour clock, blank-padded ( 0..12) - %L - Millisecond of the second (000..999) - %m - Month of the year (01..12) - %M - Minute of the hour (00..59) - %n - Newline (\n) - %N - Fractional seconds digits, default is 9 digits (nanosecond) - %3N millisecond (3 digits) - %6N microsecond (6 digits) - %9N nanosecond (9 digits) - %p - Meridian indicator (``AM'' or ``PM'') - %P - Meridian indicator (``am'' or ``pm'') - %r - time, 12-hour (same as %I:%M:%S %p) - %R - time, 24-hour (%H:%M) - %s - Number of seconds since 1970-01-01 00:00:00 UTC. - %S - Second of the minute (00..60) - %t - Tab character (\t) - %T - time, 24-hour (%H:%M:%S) - %u - Day of the week as a decimal, Monday being 1. (1..7) - %U - Week number of the current year, - starting with the first Sunday as the first - day of the first week (00..53) - %v - VMS date (%e-%b-%Y) - %V - Week number of year according to ISO 8601 (01..53) - %W - Week number of the current year, - starting with the first Monday as the first - day of the first week (00..53) - %w - Day of the week (Sunday is 0, 0..6) - %x - Preferred representation for the date alone, no time - %X - Preferred representation for the time alone, no date - %y - Year without a century (00..99) - %Y - Year with century - %z - Time zone as hour offset from UTC (e.g. +0900) - %Z - Time zone name - %% - Literal ``%'' character - EOS - ) do |arguments| - - # Technically we support two arguments but only first is mandatory ... - raise(Puppet::ParseError, "strftime(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - format = arguments[0] - - raise(Puppet::ParseError, 'strftime(): You must provide ' + - 'format for evaluation') if format.empty? - - # The Time Zone argument is optional ... - time_zone = arguments[1] if arguments[1] - - time = Time.new - - # There is probably a better way to handle Time Zone ... - if time_zone and not time_zone.empty? - original_zone = ENV['TZ'] - - local_time = time.clone - local_time = local_time.utc - - ENV['TZ'] = time_zone - - time = local_time.localtime - - ENV['TZ'] = original_zone - end - - result = time.strftime(format) - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/strip.rb b/lib/puppet/parser/functions/strip.rb deleted file mode 100644 index 3fac47d5..00000000 --- a/lib/puppet/parser/functions/strip.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -# strip.rb -# - -module Puppet::Parser::Functions - newfunction(:strip, :type => :rvalue, :doc => <<-EOS -This function removes leading and trailing whitespace from a string or from -every string inside an array. - -*Examples:* - - strip(" aaa ") - -Would result in: "aaa" - EOS - ) do |arguments| - - raise(Puppet::ParseError, "strip(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'strip(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - result = value.collect { |i| i.is_a?(String) ? i.strip : i } - else - result = value.strip - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/suffix.rb b/lib/puppet/parser/functions/suffix.rb deleted file mode 100644 index f7792d6f..00000000 --- a/lib/puppet/parser/functions/suffix.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# suffix.rb -# - -module Puppet::Parser::Functions - newfunction(:suffix, :type => :rvalue, :doc => <<-EOS -This function applies a suffix to all elements in an array. - -*Examples:* - - suffix(['a','b','c'], 'p') - -Will return: ['ap','bp','cp'] - EOS - ) do |arguments| - - # Technically we support two arguments but only first is mandatory ... - raise(Puppet::ParseError, "suffix(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - array = arguments[0] - - unless array.is_a?(Array) - raise Puppet::ParseError, "suffix(): expected first argument to be an Array, got #{array.inspect}" - end - - suffix = arguments[1] if arguments[1] - - if suffix - unless suffix.is_a? String - raise Puppet::ParseError, "suffix(): expected second argument to be a String, got #{suffix.inspect}" - end - end - - # Turn everything into string same as join would do ... - result = array.collect do |i| - i = i.to_s - suffix ? i + suffix : i - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/swapcase.rb b/lib/puppet/parser/functions/swapcase.rb deleted file mode 100644 index eb7fe137..00000000 --- a/lib/puppet/parser/functions/swapcase.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -# swapcase.rb -# - -module Puppet::Parser::Functions - newfunction(:swapcase, :type => :rvalue, :doc => <<-EOS -This function will swap the existing case of a string. - -*Examples:* - - swapcase("aBcD") - -Would result in: "AbCd" - EOS - ) do |arguments| - - raise(Puppet::ParseError, "swapcase(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'swapcase(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.swapcase : i } - else - result = value.swapcase - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/time.rb b/lib/puppet/parser/functions/time.rb deleted file mode 100644 index 0cddaf86..00000000 --- a/lib/puppet/parser/functions/time.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# time.rb -# - -module Puppet::Parser::Functions - newfunction(:time, :type => :rvalue, :doc => <<-EOS -This function will return the current time since epoch as an integer. - -*Examples:* - - time() - -Will return something like: 1311972653 - EOS - ) do |arguments| - - # The Time Zone argument is optional ... - time_zone = arguments[0] if arguments[0] - - if (arguments.size != 0) and (arguments.size != 1) then - raise(Puppet::ParseError, "time(): Wrong number of arguments "+ - "given #{arguments.size} for 0 or 1") - end - - time = Time.new - - # There is probably a better way to handle Time Zone ... - if time_zone and not time_zone.empty? - original_zone = ENV['TZ'] - - local_time = time.clone - local_time = local_time.utc - - ENV['TZ'] = time_zone - - time = local_time.localtime - - ENV['TZ'] = original_zone - end - - # Calling Time#to_i on a receiver changes it. Trust me I am the Doctor. - result = time.strftime('%s') - result = result.to_i - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/to_bytes.rb b/lib/puppet/parser/functions/to_bytes.rb deleted file mode 100644 index df490ea8..00000000 --- a/lib/puppet/parser/functions/to_bytes.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:to_bytes, :type => :rvalue, :doc => <<-EOS - Converts the argument into bytes, for example 4 kB becomes 4096. - Takes a single string value as an argument. - These conversions reflect a layperson's understanding of - 1 MB = 1024 KB, when in fact 1 MB = 1000 KB, and 1 MiB = 1024 KiB. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "to_bytes(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size != 1 - - arg = arguments[0] - - return arg if arg.is_a? Numeric - - value,prefix = */([0-9.e+-]*)\s*([^bB]?)/.match(arg)[1,2] - - value = value.to_f - case prefix - when '' then return value.to_i - when 'k' then return (value*(1<<10)).to_i - when 'M' then return (value*(1<<20)).to_i - when 'G' then return (value*(1<<30)).to_i - when 'T' then return (value*(1<<40)).to_i - when 'P' then return (value*(1<<50)).to_i - when 'E' then return (value*(1<<60)).to_i - else raise Puppet::ParseError, "to_bytes(): Unknown prefix #{prefix}" - end - end -end diff --git a/lib/puppet/parser/functions/type.rb b/lib/puppet/parser/functions/type.rb deleted file mode 100644 index 016529b0..00000000 --- a/lib/puppet/parser/functions/type.rb +++ /dev/null @@ -1,19 +0,0 @@ -# -# type.rb -# - -module Puppet::Parser::Functions - newfunction(:type, :type => :rvalue, :doc => <<-EOS - DEPRECATED: This function will cease to function on Puppet 4; please use type3x() before upgrading to puppet 4 for backwards-compatibility, or migrate to the new parser's typing system. - EOS - ) do |args| - - warning("type() DEPRECATED: This function will cease to function on Puppet 4; please use type3x() before upgrading to puppet 4 for backwards-compatibility, or migrate to the new parser's typing system.") - if ! Puppet::Parser::Functions.autoloader.loaded?(:type3x) - Puppet::Parser::Functions.autoloader.load(:type3x) - end - function_type3x(args + [false]) - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/type3x.rb b/lib/puppet/parser/functions/type3x.rb deleted file mode 100644 index 0800b4a3..00000000 --- a/lib/puppet/parser/functions/type3x.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -# type3x.rb -# - -module Puppet::Parser::Functions - newfunction(:type3x, :type => :rvalue, :doc => <<-EOS -DEPRECATED: This function will be removed when puppet 3 support is dropped; please migrate to the new parser's typing system. - -Returns the type when passed a value. Type can be one of: - -* string -* array -* hash -* float -* integer -* boolean - EOS - ) do |args| - raise(Puppet::ParseError, "type3x(): Wrong number of arguments " + - "given (#{args.size} for 1)") if args.size < 1 - - value = args[0] - - klass = value.class - - if not [TrueClass, FalseClass, Array, Bignum, Fixnum, Float, Hash, String].include?(klass) - raise(Puppet::ParseError, 'type3x(): Unknown type') - end - - klass = klass.to_s # Ugly ... - - # We note that Integer is the parent to Bignum and Fixnum ... - result = case klass - when /^(?:Big|Fix)num$/ then 'integer' - when /^(?:True|False)Class$/ then 'boolean' - else klass - end - - if result == "String" then - if value == value.to_i.to_s then - result = "Integer" - elsif value == value.to_f.to_s then - result = "Float" - end - end - - return result.downcase - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/union.rb b/lib/puppet/parser/functions/union.rb deleted file mode 100644 index c91bb805..00000000 --- a/lib/puppet/parser/functions/union.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# union.rb -# - -module Puppet::Parser::Functions - newfunction(:union, :type => :rvalue, :doc => <<-EOS -This function returns a union of two arrays. - -*Examples:* - - union(["a","b","c"],["b","c","d"]) - -Would return: ["a","b","c","d"] - EOS - ) do |arguments| - - # Two arguments are required - raise(Puppet::ParseError, "union(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size != 2 - - first = arguments[0] - second = arguments[1] - - unless first.is_a?(Array) && second.is_a?(Array) - raise(Puppet::ParseError, 'union(): Requires 2 arrays') - end - - result = first | second - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/unique.rb b/lib/puppet/parser/functions/unique.rb deleted file mode 100644 index cf770f3b..00000000 --- a/lib/puppet/parser/functions/unique.rb +++ /dev/null @@ -1,50 +0,0 @@ -# -# unique.rb -# - -module Puppet::Parser::Functions - newfunction(:unique, :type => :rvalue, :doc => <<-EOS -This function will remove duplicates from strings and arrays. - -*Examples:* - - unique("aabbcc") - -Will return: - - abc - -You can also use this with arrays: - - unique(["a","a","b","b","c","c"]) - -This returns: - - ["a","b","c"] - EOS - ) do |arguments| - - raise(Puppet::ParseError, "unique(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'unique(): Requires either ' + - 'array or string to work with') - end - - result = value.clone - - string = value.is_a?(String) ? true : false - - # We turn any string value into an array to be able to shuffle ... - result = string ? result.split('') : result - result = result.uniq # Remove duplicates ... - result = string ? result.join : result - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/upcase.rb b/lib/puppet/parser/functions/upcase.rb deleted file mode 100644 index 4302b29e..00000000 --- a/lib/puppet/parser/functions/upcase.rb +++ /dev/null @@ -1,40 +0,0 @@ -# -# upcase.rb -# - -module Puppet::Parser::Functions - newfunction(:upcase, :type => :rvalue, :doc => <<-EOS -Converts a string or an array of strings to uppercase. - -*Examples:* - - upcase("abcd") - -Will return: - - ASDF - EOS - ) do |arguments| - - raise(Puppet::ParseError, "upcase(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'upcase(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? i.upcase : i } - else - result = value.upcase - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/uriescape.rb b/lib/puppet/parser/functions/uriescape.rb deleted file mode 100644 index a486eee5..00000000 --- a/lib/puppet/parser/functions/uriescape.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# uriescape.rb -# -require 'uri' - -module Puppet::Parser::Functions - newfunction(:uriescape, :type => :rvalue, :doc => <<-EOS - Urlencodes a string or array of strings. - Requires either a single string or an array as an input. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "uriescape(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - value = arguments[0] - - unless value.is_a?(Array) || value.is_a?(String) - raise(Puppet::ParseError, 'uriescape(): Requires either ' + - 'array or string to work with') - end - - if value.is_a?(Array) - # Numbers in Puppet are often string-encoded which is troublesome ... - result = value.collect { |i| i.is_a?(String) ? URI.escape(i,unsafe) : i } - else - result = URI.escape(value) - end - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/validate_absolute_path.rb b/lib/puppet/parser/functions/validate_absolute_path.rb deleted file mode 100644 index b6966809..00000000 --- a/lib/puppet/parser/functions/validate_absolute_path.rb +++ /dev/null @@ -1,69 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:validate_absolute_path, :doc => <<-'ENDHEREDOC') do |args| - Validate the string represents an absolute path in the filesystem. This function works - for windows and unix style paths. - - The following values will pass: - - $my_path = 'C:/Program Files (x86)/Puppet Labs/Puppet' - validate_absolute_path($my_path) - $my_path2 = '/var/lib/puppet' - validate_absolute_path($my_path2) - $my_path3 = ['C:/Program Files (x86)/Puppet Labs/Puppet','C:/Program Files/Puppet Labs/Puppet'] - validate_absolute_path($my_path3) - $my_path4 = ['/var/lib/puppet','/usr/share/puppet'] - validate_absolute_path($my_path4) - - The following values will fail, causing compilation to abort: - - validate_absolute_path(true) - validate_absolute_path('../var/lib/puppet') - validate_absolute_path('var/lib/puppet') - validate_absolute_path([ 'var/lib/puppet', '/var/foo' ]) - validate_absolute_path([ '/var/lib/puppet', 'var/foo' ]) - $undefined = undef - validate_absolute_path($undefined) - - ENDHEREDOC - - require 'puppet/util' - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_absolute_path(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - # put arg to candidate var to be able to replace it - candidates = arg - # if arg is just a string with a path to test, convert it to an array - # to avoid test code duplication - unless arg.is_a?(Array) then - candidates = Array.new(1,arg) - end - # iterate over all pathes within the candidates array - candidates.each do |path| - # This logic was borrowed from - # [lib/puppet/file_serving/base.rb](https://github.com/puppetlabs/puppet/blob/master/lib/puppet/file_serving/base.rb) - # Puppet 2.7 and beyond will have Puppet::Util.absolute_path? Fall back to a back-ported implementation otherwise. - if Puppet::Util.respond_to?(:absolute_path?) then - unless Puppet::Util.absolute_path?(path, :posix) or Puppet::Util.absolute_path?(path, :windows) - raise Puppet::ParseError, ("#{path.inspect} is not an absolute path.") - end - else - # This code back-ported from 2.7.x's lib/puppet/util.rb Puppet::Util.absolute_path? - # Determine in a platform-specific way whether a path is absolute. This - # defaults to the local platform if none is specified. - # Escape once for the string literal, and once for the regex. - slash = '[\\\\/]' - name = '[^\\\\/]+' - regexes = { - :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, - :posix => %r!^/!, - } - rval = (!!(path =~ regexes[:posix])) || (!!(path =~ regexes[:windows])) - rval or raise Puppet::ParseError, ("#{path.inspect} is not an absolute path.") - end - end - end - end -end diff --git a/lib/puppet/parser/functions/validate_array.rb b/lib/puppet/parser/functions/validate_array.rb deleted file mode 100644 index 34b51182..00000000 --- a/lib/puppet/parser/functions/validate_array.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_array, :doc => <<-'ENDHEREDOC') do |args| - Validate that all passed values are array data structures. Abort catalog - compilation if any value fails this check. - - The following values will pass: - - $my_array = [ 'one', 'two' ] - validate_array($my_array) - - The following values will fail, causing compilation to abort: - - validate_array(true) - validate_array('some_string') - $undefined = undef - validate_array($undefined) - - ENDHEREDOC - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_array(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless arg.is_a?(Array) - raise Puppet::ParseError, ("#{arg.inspect} is not an Array. It looks to be a #{arg.class}") - end - end - - end - -end diff --git a/lib/puppet/parser/functions/validate_augeas.rb b/lib/puppet/parser/functions/validate_augeas.rb deleted file mode 100644 index 4ea4fe07..00000000 --- a/lib/puppet/parser/functions/validate_augeas.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'tempfile' - -module Puppet::Parser::Functions - newfunction(:validate_augeas, :doc => <<-'ENDHEREDOC') do |args| - Perform validation of a string using an Augeas lens - The first argument of this function should be a string to - test, and the second argument should be the name of the Augeas lens to use. - If Augeas fails to parse the string with the lens, the compilation will - abort with a parse error. - - A third argument can be specified, listing paths which should - not be found in the file. The `$file` variable points to the location - of the temporary file being tested in the Augeas tree. - - For example, if you want to make sure your passwd content never contains - a user `foo`, you could write: - - validate_augeas($passwdcontent, 'Passwd.lns', ['$file/foo']) - - Or if you wanted to ensure that no users used the '/bin/barsh' shell, - you could use: - - validate_augeas($passwdcontent, 'Passwd.lns', ['$file/*[shell="/bin/barsh"]'] - - If a fourth argument is specified, this will be the error message raised and - seen by the user. - - A helpful error message can be returned like this: - - validate_augeas($sudoerscontent, 'Sudoers.lns', [], 'Failed to validate sudoers content with Augeas') - - ENDHEREDOC - unless Puppet.features.augeas? - raise Puppet::ParseError, ("validate_augeas(): this function requires the augeas feature. See http://projects.puppetlabs.com/projects/puppet/wiki/Puppet_Augeas#Pre-requisites for how to activate it.") - end - - if (args.length < 2) or (args.length > 4) then - raise Puppet::ParseError, ("validate_augeas(): wrong number of arguments (#{args.length}; must be 2, 3, or 4)") - end - - msg = args[3] || "validate_augeas(): Failed to validate content against #{args[1].inspect}" - - require 'augeas' - aug = Augeas::open(nil, nil, Augeas::NO_MODL_AUTOLOAD) - begin - content = args[0] - - # Test content in a temporary file - tmpfile = Tempfile.new("validate_augeas") - begin - tmpfile.write(content) - ensure - tmpfile.close - end - - # Check for syntax - lens = args[1] - aug.transform( - :lens => lens, - :name => 'Validate_augeas', - :incl => tmpfile.path - ) - aug.load! - - unless aug.match("/augeas/files#{tmpfile.path}//error").empty? - error = aug.get("/augeas/files#{tmpfile.path}//error/message") - msg += " with error: #{error}" - raise Puppet::ParseError, (msg) - end - - # Launch unit tests - tests = args[2] || [] - aug.defvar('file', "/files#{tmpfile.path}") - tests.each do |t| - msg += " testing path #{t}" - raise Puppet::ParseError, (msg) unless aug.match(t).empty? - end - ensure - aug.close - tmpfile.unlink - end - end -end diff --git a/lib/puppet/parser/functions/validate_bool.rb b/lib/puppet/parser/functions/validate_bool.rb deleted file mode 100644 index 59a08056..00000000 --- a/lib/puppet/parser/functions/validate_bool.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_bool, :doc => <<-'ENDHEREDOC') do |args| - Validate that all passed values are either true or false. Abort catalog - compilation if any value fails this check. - - The following values will pass: - - $iamtrue = true - validate_bool(true) - validate_bool(true, true, false, $iamtrue) - - The following values will fail, causing compilation to abort: - - $some_array = [ true ] - validate_bool("false") - validate_bool("true") - validate_bool($some_array) - - ENDHEREDOC - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_bool(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless function_is_bool([arg]) - raise Puppet::ParseError, ("#{arg.inspect} is not a boolean. It looks to be a #{arg.class}") - end - end - - end - -end diff --git a/lib/puppet/parser/functions/validate_cmd.rb b/lib/puppet/parser/functions/validate_cmd.rb deleted file mode 100644 index 5df3c609..00000000 --- a/lib/puppet/parser/functions/validate_cmd.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'puppet/util/execution' -require 'tempfile' - -module Puppet::Parser::Functions - newfunction(:validate_cmd, :doc => <<-'ENDHEREDOC') do |args| - Perform validation of a string with an external command. - The first argument of this function should be a string to - test, and the second argument should be a path to a test command - taking a % as a placeholder for the file path (will default to the end). - If the command, launched against a tempfile containing the passed string, - returns a non-null value, compilation will abort with a parse error. - - If a third argument is specified, this will be the error message raised and - seen by the user. - - A helpful error message can be returned like this: - - Example: - - # Defaults to end of path - validate_cmd($sudoerscontent, '/usr/sbin/visudo -c -f', 'Visudo failed to validate sudoers content') - - # % as file location - validate_cmd($haproxycontent, '/usr/sbin/haproxy -f % -c', 'Haproxy failed to validate config content') - - ENDHEREDOC - if (args.length < 2) or (args.length > 3) then - raise Puppet::ParseError, ("validate_cmd(): wrong number of arguments (#{args.length}; must be 2 or 3)") - end - - msg = args[2] || "validate_cmd(): failed to validate content with command #{args[1].inspect}" - - content = args[0] - checkscript = args[1] - - # Test content in a temporary file - tmpfile = Tempfile.new("validate_cmd") - begin - tmpfile.write(content) - tmpfile.close - - if checkscript =~ /\s%(\s|$)/ - check_with_correct_location = checkscript.gsub(/%/,tmpfile.path) - else - check_with_correct_location = "#{checkscript} #{tmpfile.path}" - end - - if Puppet::Util::Execution.respond_to?('execute') - Puppet::Util::Execution.execute(check_with_correct_location) - else - Puppet::Util.execute(check_with_correct_location) - end - rescue Puppet::ExecutionFailure => detail - msg += "\n#{detail}" - raise Puppet::ParseError, msg - rescue Exception => detail - msg += "\n#{detail.class.name} #{detail}" - raise Puppet::ParseError, msg - ensure - tmpfile.unlink - end - end -end diff --git a/lib/puppet/parser/functions/validate_hash.rb b/lib/puppet/parser/functions/validate_hash.rb deleted file mode 100644 index 9bdd5432..00000000 --- a/lib/puppet/parser/functions/validate_hash.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_hash, :doc => <<-'ENDHEREDOC') do |args| - Validate that all passed values are hash data structures. Abort catalog - compilation if any value fails this check. - - The following values will pass: - - $my_hash = { 'one' => 'two' } - validate_hash($my_hash) - - The following values will fail, causing compilation to abort: - - validate_hash(true) - validate_hash('some_string') - $undefined = undef - validate_hash($undefined) - - ENDHEREDOC - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_hash(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless arg.is_a?(Hash) - raise Puppet::ParseError, ("#{arg.inspect} is not a Hash. It looks to be a #{arg.class}") - end - end - - end - -end diff --git a/lib/puppet/parser/functions/validate_ipv4_address.rb b/lib/puppet/parser/functions/validate_ipv4_address.rb deleted file mode 100644 index fc02748e..00000000 --- a/lib/puppet/parser/functions/validate_ipv4_address.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_ipv4_address, :doc => <<-ENDHEREDOC - Validate that all values passed are valid IPv4 addresses. - Fail compilation if any value fails this check. - - The following values will pass: - - $my_ip = "1.2.3.4" - validate_ipv4_address($my_ip) - validate_bool("8.8.8.8", "172.16.0.1", $my_ip) - - The following values will fail, causing compilation to abort: - - $some_array = [ 1, true, false, "garbage string", "3ffe:505:2" ] - validate_ipv4_address($some_array) - - ENDHEREDOC - ) do |args| - - require "ipaddr" - rescuable_exceptions = [ ArgumentError ] - - if defined?(IPAddr::InvalidAddressError) - rescuable_exceptions << IPAddr::InvalidAddressError - end - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_ipv4_address(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless arg.is_a?(String) - raise Puppet::ParseError, "#{arg.inspect} is not a string." - end - - begin - unless IPAddr.new(arg).ipv4? - raise Puppet::ParseError, "#{arg.inspect} is not a valid IPv4 address." - end - rescue *rescuable_exceptions - raise Puppet::ParseError, "#{arg.inspect} is not a valid IPv4 address." - end - end - - end - -end diff --git a/lib/puppet/parser/functions/validate_ipv6_address.rb b/lib/puppet/parser/functions/validate_ipv6_address.rb deleted file mode 100644 index b0f2558d..00000000 --- a/lib/puppet/parser/functions/validate_ipv6_address.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_ipv6_address, :doc => <<-ENDHEREDOC - Validate that all values passed are valid IPv6 addresses. - Fail compilation if any value fails this check. - - The following values will pass: - - $my_ip = "3ffe:505:2" - validate_ipv6_address(1) - validate_ipv6_address($my_ip) - validate_bool("fe80::baf6:b1ff:fe19:7507", $my_ip) - - The following values will fail, causing compilation to abort: - - $some_array = [ true, false, "garbage string", "1.2.3.4" ] - validate_ipv6_address($some_array) - - ENDHEREDOC - ) do |args| - - require "ipaddr" - rescuable_exceptions = [ ArgumentError ] - - if defined?(IPAddr::InvalidAddressError) - rescuable_exceptions << IPAddr::InvalidAddressError - end - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_ipv6_address(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless arg.is_a?(String) - raise Puppet::ParseError, "#{arg.inspect} is not a string." - end - - begin - unless IPAddr.new(arg).ipv6? - raise Puppet::ParseError, "#{arg.inspect} is not a valid IPv6 address." - end - rescue *rescuable_exceptions - raise Puppet::ParseError, "#{arg.inspect} is not a valid IPv6 address." - end - end - - end - -end diff --git a/lib/puppet/parser/functions/validate_re.rb b/lib/puppet/parser/functions/validate_re.rb deleted file mode 100644 index ca25a702..00000000 --- a/lib/puppet/parser/functions/validate_re.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Puppet::Parser::Functions - newfunction(:validate_re, :doc => <<-'ENDHEREDOC') do |args| - Perform simple validation of a string against one or more regular - expressions. The first argument of this function should be a string to - test, and the second argument should be a stringified regular expression - (without the // delimiters) or an array of regular expressions. If none - of the regular expressions match the string passed in, compilation will - abort with a parse error. - - If a third argument is specified, this will be the error message raised and - seen by the user. - - The following strings will validate against the regular expressions: - - validate_re('one', '^one$') - validate_re('one', [ '^one', '^two' ]) - - The following strings will fail to validate, causing compilation to abort: - - validate_re('one', [ '^two', '^three' ]) - - A helpful error message can be returned like this: - - validate_re($::puppetversion, '^2.7', 'The $puppetversion fact value does not match 2.7') - - ENDHEREDOC - if (args.length < 2) or (args.length > 3) then - raise Puppet::ParseError, ("validate_re(): wrong number of arguments (#{args.length}; must be 2 or 3)") - end - - msg = args[2] || "validate_re(): #{args[0].inspect} does not match #{args[1].inspect}" - - # We're using a flattened array here because we can't call String#any? in - # Ruby 1.9 like we can in Ruby 1.8 - raise Puppet::ParseError, (msg) unless [args[1]].flatten.any? do |re_str| - args[0] =~ Regexp.compile(re_str) - end - - end -end diff --git a/lib/puppet/parser/functions/validate_slength.rb b/lib/puppet/parser/functions/validate_slength.rb deleted file mode 100644 index 7d534f37..00000000 --- a/lib/puppet/parser/functions/validate_slength.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_slength, :doc => <<-'ENDHEREDOC') do |args| - Validate that the first argument is a string (or an array of strings), and - less/equal to than the length of the second argument. An optional third - parameter can be given a the minimum length. It fails if the first - argument is not a string or array of strings, and if arg 2 and arg 3 are - not convertable to a number. - - The following values will pass: - - validate_slength("discombobulate",17) - validate_slength(["discombobulate","moo"],17) - validate_slength(["discombobulate","moo"],17,3) - - The following valueis will not: - - validate_slength("discombobulate",1) - validate_slength(["discombobulate","thermometer"],5) - validate_slength(["discombobulate","moo"],17,10) - - ENDHEREDOC - - raise Puppet::ParseError, "validate_slength(): Wrong number of arguments (#{args.length}; must be 2 or 3)" unless args.length == 2 or args.length == 3 - - input, max_length, min_length = *args - - begin - max_length = Integer(max_length) - raise ArgumentError if max_length <= 0 - rescue ArgumentError, TypeError - raise Puppet::ParseError, "validate_slength(): Expected second argument to be a positive Numeric, got #{max_length}:#{max_length.class}" - end - - if min_length - begin - min_length = Integer(min_length) - raise ArgumentError if min_length < 0 - rescue ArgumentError, TypeError - raise Puppet::ParseError, "validate_slength(): Expected third argument to be unset or a positive Numeric, got #{min_length}:#{min_length.class}" - end - else - min_length = 0 - end - - if min_length > max_length - raise Puppet::ParseError, "validate_slength(): Expected second argument to be larger than third argument" - end - - validator = lambda do |str| - unless str.length <= max_length and str.length >= min_length - raise Puppet::ParseError, "validate_slength(): Expected length of #{input.inspect} to be between #{min_length} and #{max_length}, was #{input.length}" - end - end - - case input - when String - validator.call(input) - when Array - input.each_with_index do |arg, pos| - if arg.is_a? String - validator.call(arg) - else - raise Puppet::ParseError, "validate_slength(): Expected element at array position #{pos} to be a String, got #{arg.class}" - end - end - else - raise Puppet::ParseError, "validate_slength(): Expected first argument to be a String or Array, got #{input.class}" - end - end -end diff --git a/lib/puppet/parser/functions/validate_string.rb b/lib/puppet/parser/functions/validate_string.rb deleted file mode 100644 index c841f6ab..00000000 --- a/lib/puppet/parser/functions/validate_string.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Puppet::Parser::Functions - - newfunction(:validate_string, :doc => <<-'ENDHEREDOC') do |args| - Validate that all passed values are string data structures. Abort catalog - compilation if any value fails this check. - - The following values will pass: - - $my_string = "one two" - validate_string($my_string, 'three') - - The following values will fail, causing compilation to abort: - - validate_string(true) - validate_string([ 'some', 'array' ]) - - Note: validate_string(undef) will not fail in this version of the - functions API (incl. current and future parser). Instead, use: - - if $var == undef { - fail('...') - } - - ENDHEREDOC - - unless args.length > 0 then - raise Puppet::ParseError, ("validate_string(): wrong number of arguments (#{args.length}; must be > 0)") - end - - args.each do |arg| - unless arg.is_a?(String) - raise Puppet::ParseError, ("#{arg.inspect} is not a string. It looks to be a #{arg.class}") - end - end - - end - -end diff --git a/lib/puppet/parser/functions/values.rb b/lib/puppet/parser/functions/values.rb deleted file mode 100644 index 16067561..00000000 --- a/lib/puppet/parser/functions/values.rb +++ /dev/null @@ -1,39 +0,0 @@ -# -# values.rb -# - -module Puppet::Parser::Functions - newfunction(:values, :type => :rvalue, :doc => <<-EOS -When given a hash this function will return the values of that hash. - -*Examples:* - - $hash = { - 'a' => 1, - 'b' => 2, - 'c' => 3, - } - values($hash) - -This example would return: - - [1,2,3] - EOS - ) do |arguments| - - raise(Puppet::ParseError, "values(): Wrong number of arguments " + - "given (#{arguments.size} for 1)") if arguments.size < 1 - - hash = arguments[0] - - unless hash.is_a?(Hash) - raise(Puppet::ParseError, 'values(): Requires hash to work with') - end - - result = hash.values - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/values_at.rb b/lib/puppet/parser/functions/values_at.rb deleted file mode 100644 index f350f539..00000000 --- a/lib/puppet/parser/functions/values_at.rb +++ /dev/null @@ -1,99 +0,0 @@ -# -# values_at.rb -# - -module Puppet::Parser::Functions - newfunction(:values_at, :type => :rvalue, :doc => <<-EOS -Finds value inside an array based on location. - -The first argument is the array you want to analyze, and the second element can -be a combination of: - -* A single numeric index -* A range in the form of 'start-stop' (eg. 4-9) -* An array combining the above - -*Examples*: - - values_at(['a','b','c'], 2) - -Would return ['c']. - - values_at(['a','b','c'], ["0-1"]) - -Would return ['a','b']. - - values_at(['a','b','c','d','e'], [0, "2-3"]) - -Would return ['a','c','d']. - EOS - ) do |arguments| - - raise(Puppet::ParseError, "values_at(): Wrong number of " + - "arguments given (#{arguments.size} for 2)") if arguments.size < 2 - - array = arguments.shift - - unless array.is_a?(Array) - raise(Puppet::ParseError, 'values_at(): Requires array to work with') - end - - indices = [arguments.shift].flatten() # Get them all ... Pokemon ... - - if not indices or indices.empty? - raise(Puppet::ParseError, 'values_at(): You must provide ' + - 'at least one positive index to collect') - end - - result = [] - indices_list = [] - - indices.each do |i| - i = i.to_s - if m = i.match(/^(\d+)(\.\.\.?|\-)(\d+)$/) - start = m[1].to_i - stop = m[3].to_i - - type = m[2] - - if start > stop - raise(Puppet::ParseError, 'values_at(): Stop index in ' + - 'given indices range is smaller than the start index') - elsif stop > array.size - 1 # First element is at index 0 is it not? - raise(Puppet::ParseError, 'values_at(): Stop index in ' + - 'given indices range exceeds array size') - end - - range = case type - when /^(\.\.|\-)$/ then (start .. stop) - when /^(\.\.\.)$/ then (start ... stop) # Exclusive of last element ... - end - - range.each { |i| indices_list << i.to_i } - else - # Only positive numbers allowed in this case ... - if not i.match(/^\d+$/) - raise(Puppet::ParseError, 'values_at(): Unknown format ' + - 'of given index') - end - - # In Puppet numbers are often string-encoded ... - i = i.to_i - - if i > array.size - 1 # Same story. First element is at index 0 ... - raise(Puppet::ParseError, 'values_at(): Given index ' + - 'exceeds array size') - end - - indices_list << i - end - end - - # We remove nil values as they make no sense in Puppet DSL ... - result = indices_list.collect { |i| array[i] }.compact - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/parser/functions/zip.rb b/lib/puppet/parser/functions/zip.rb deleted file mode 100644 index 3074f282..00000000 --- a/lib/puppet/parser/functions/zip.rb +++ /dev/null @@ -1,39 +0,0 @@ -# -# zip.rb -# - -module Puppet::Parser::Functions - newfunction(:zip, :type => :rvalue, :doc => <<-EOS -Takes one element from first array and merges corresponding elements from second array. This generates a sequence of n-element arrays, where n is one more than the count of arguments. - -*Example:* - - zip(['1','2','3'],['4','5','6']) - -Would result in: - - ["1", "4"], ["2", "5"], ["3", "6"] - EOS - ) do |arguments| - - # Technically we support three arguments but only first is mandatory ... - raise(Puppet::ParseError, "zip(): Wrong number of arguments " + - "given (#{arguments.size} for 2)") if arguments.size < 2 - - a = arguments[0] - b = arguments[1] - - unless a.is_a?(Array) and b.is_a?(Array) - raise(Puppet::ParseError, 'zip(): Requires array to work with') - end - - flatten = function_str2bool([arguments[2]]) if arguments[2] - - result = a.zip(b) - result = flatten ? result.flatten : result - - return result - end -end - -# vim: set ts=2 sw=2 et : diff --git a/lib/puppet/provider/file_line/ruby.rb b/lib/puppet/provider/file_line/ruby.rb deleted file mode 100644 index ae1a8b3d..00000000 --- a/lib/puppet/provider/file_line/ruby.rb +++ /dev/null @@ -1,85 +0,0 @@ -Puppet::Type.type(:file_line).provide(:ruby) do - def exists? - lines.find do |line| - line.chomp == resource[:line].chomp - end - end - - def create - if resource[:match] - handle_create_with_match - elsif resource[:after] - handle_create_with_after - else - append_line - end - end - - def destroy - local_lines = lines - File.open(resource[:path],'w') do |fh| - fh.write(local_lines.reject{|l| l.chomp == resource[:line] }.join('')) - end - end - - private - def lines - # If this type is ever used with very large files, we should - # write this in a different way, using a temp - # file; for now assuming that this type is only used on - # small-ish config files that can fit into memory without - # too much trouble. - @lines ||= File.readlines(resource[:path]) - end - - def handle_create_with_match() - regex = resource[:match] ? Regexp.new(resource[:match]) : nil - match_count = count_matches(regex) - if match_count > 1 && resource[:multiple].to_s != 'true' - raise Puppet::Error, "More than one line in file '#{resource[:path]}' matches pattern '#{resource[:match]}'" - end - File.open(resource[:path], 'w') do |fh| - lines.each do |l| - fh.puts(regex.match(l) ? resource[:line] : l) - end - - if (match_count == 0) - fh.puts(resource[:line]) - end - end - end - - def handle_create_with_after - regex = Regexp.new(resource[:after]) - count = count_matches(regex) - case count - when 1 # find the line to put our line after - File.open(resource[:path], 'w') do |fh| - lines.each do |l| - fh.puts(l) - if regex.match(l) then - fh.puts(resource[:line]) - end - end - end - when 0 # append the line to the end of the file - append_line - else - raise Puppet::Error, "#{count} lines match pattern '#{resource[:after]}' in file '#{resource[:path]}'. One or no line must match the pattern." - end - end - - def count_matches(regex) - lines.select{|l| l.match(regex)}.size - end - - ## - # append the line to the file. - # - # @api private - def append_line - File.open(resource[:path], 'a') do |fh| - fh.puts resource[:line] - end - end -end diff --git a/lib/puppet/type/anchor.rb b/lib/puppet/type/anchor.rb deleted file mode 100644 index fe1e5aa1..00000000 --- a/lib/puppet/type/anchor.rb +++ /dev/null @@ -1,46 +0,0 @@ -Puppet::Type.newtype(:anchor) do - desc <<-'ENDOFDESC' - A simple resource type intended to be used as an anchor in a composite class. - - In Puppet 2.6, when a class declares another class, the resources in the - interior class are not contained by the exterior class. This interacts badly - with the pattern of composing complex modules from smaller classes, as it - makes it impossible for end users to specify order relationships between the - exterior class and other modules. - - The anchor type lets you work around this. By sandwiching any interior - classes between two no-op resources that _are_ contained by the exterior - class, you can ensure that all resources in the module are contained. - - class ntp { - # These classes will have the correct order relationship with each - # other. However, without anchors, they won't have any order - # relationship to Class['ntp']. - class { 'ntp::package': } - -> class { 'ntp::config': } - -> class { 'ntp::service': } - - # These two resources "anchor" the composed classes within the ntp - # class. - anchor { 'ntp::begin': } -> Class['ntp::package'] - Class['ntp::service'] -> anchor { 'ntp::end': } - } - - This allows the end user of the ntp module to establish require and before - relationships with Class['ntp']: - - class { 'ntp': } -> class { 'mcollective': } - class { 'mcollective': } -> class { 'ntp': } - - ENDOFDESC - - newparam :name do - desc "The name of the anchor resource." - end - - def refresh - # We don't do anything with them, but we need this to - # show that we are "refresh aware" and not break the - # chain of propagation. - end -end diff --git a/lib/puppet/type/file_line.rb b/lib/puppet/type/file_line.rb deleted file mode 100644 index df263e6a..00000000 --- a/lib/puppet/type/file_line.rb +++ /dev/null @@ -1,75 +0,0 @@ -Puppet::Type.newtype(:file_line) do - - desc <<-EOT - Ensures that a given line is contained within a file. The implementation - matches the full line, including whitespace at the beginning and end. If - the line is not contained in the given file, Puppet will add the line to - ensure the desired state. Multiple resources may be declared to manage - multiple lines in the same file. - - Example: - - file_line { 'sudo_rule': - path => '/etc/sudoers', - line => '%sudo ALL=(ALL) ALL', - } - file_line { 'sudo_rule_nopw': - path => '/etc/sudoers', - line => '%sudonopw ALL=(ALL) NOPASSWD: ALL', - } - - In this example, Puppet will ensure both of the specified lines are - contained in the file /etc/sudoers. - - **Autorequires:** If Puppet is managing the file that will contain the line - being managed, the file_line resource will autorequire that file. - - EOT - - ensurable do - defaultvalues - defaultto :present - end - - newparam(:name, :namevar => true) do - desc 'An arbitrary name used as the identity of the resource.' - end - - newparam(:match) do - desc 'An optional regular expression to run against existing lines in the file;\n' + - 'if a match is found, we replace that line rather than adding a new line.' - end - - newparam(:multiple) do - desc 'An optional value to determine if match can change multiple lines.' - newvalues(true, false) - end - - newparam(:after) do - desc 'An optional value used to specify the line after which we will add any new lines. (Existing lines are added in place)' - end - - newparam(:line) do - desc 'The line to be appended to the file located by the path parameter.' - end - - newparam(:path) do - desc 'The file Puppet will ensure contains the line specified by the line parameter.' - validate do |value| - unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) - raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'") - end - end - end - - # Autorequire the file resource if it's being managed - autorequire(:file) do - self[:path] - end - - validate do - unless self[:line] and self[:path] - raise(Puppet::Error, "Both line and path are required attributes") - end - end -end |