class MU::Groomer::Ansible

Support for Ansible as a host configuration management layer.

Constants

BINDIR

Location in which we'll find our Ansible executables. This only applies to full-grown Mu masters; minimalist gem installs will have to make do with whatever Ansible executables they can find in $PATH.

Public Class Methods

ansibleExecDir() click to toggle source

Hunt down and return a path for Ansible executables @return [String]

# File modules/mu/groomers/ansible.rb, line 495
def self.ansibleExecDir
  path = nil
  if File.exist?(BINDIR+"/ansible-playbook")
    path = BINDIR
  else
    paths = ENV['PATH'].split(/:/)
    paths << "/usr/bin"
    paths.uniq.each { |bindir|
      if File.exist?(bindir+"/ansible-playbook")
        path = bindir
        if !File.exist?(bindir+"/ansible-vault")
          MU.log "Found ansible-playbook executable in #{bindir}, but no ansible-vault. Vault functionality will not work!", MU::WARN
        end
        if !File.exist?(bindir+"/ansible-galaxy")
          MU.log "Found ansible-playbook executable in #{bindir}, but no ansible-galaxy. Automatic community role fetch will not work!", MU::WARN
        end
        break
      end
    }
  end
  path
end
available?(windows = false) click to toggle source

Are Ansible executables and key libraries present and accounted for?

# File modules/mu/groomers/ansible.rb, line 66
def self.available?(windows = false)
  MU::Groomer::Ansible.checkPythonDependencies(windows)
end
checkPythonDependencies(windows = false) click to toggle source

Make sure what's in our Python requirements.txt is reflected in the Python we're about to run for Ansible

# File modules/mu/groomers/ansible.rb, line 473
def self.checkPythonDependencies(windows = false)
  return nil if !ansibleExecDir

  execline = File.readlines(ansibleExecDir+"/ansible-playbook").first.chomp.sub(/^#!/, '')
  if !execline
    MU.log "Unable to extract a Python executable from #{ansibleExecDir}/ansible-playbook", MU::ERR
    return false
  end

  require 'tempfile'
  f = Tempfile.new("pythoncheck")
  f.puts "import ansible"
  f.puts "import winrm" if windows
  f.close

  system(%Q{#{execline} #{f.path}})
  f.unlink
  $?.exitstatus == 0 ? true : false
end
cleanup(deploy_id, noop = false) click to toggle source

Nuke everything associated with a deploy. Since we're just some files in the deploy directory, this doesn't have to do anything.

# File modules/mu/groomers/ansible.rb, line 404
      def self.cleanup(deploy_id, noop = false)
#        deploy = MU::MommaCat.new(MU.deploy_id)
#        inventory = Inventory.new(deploy)
      end
deleteSecret(vault: nil, item: nil) click to toggle source

Delete a Ansible data bag / Vault @param vault [String]: A repository of secrets to delete

# File modules/mu/groomers/ansible.rb, line 196
def self.deleteSecret(vault: nil, item: nil)
  if vault.nil? or vault.empty?
    raise MuError, "Must call deleteSecret with at least a vault name"
  end
  dir = secret_dir+"/"+vault
  if !Dir.exist?(dir)
    raise MuNoSuchSecret, "No such vault #{vault}"
  end

  if item
    itempath = dir+"/"+item
    if !File.exist?(itempath)
      raise MuNoSuchSecret, "No such item #{item} in vault #{vault}"
    end
    MU.log "Deleting Ansible vault #{vault} item #{item}", MU::NOTICE
    File.unlink(itempath)
  else
    MU.log "Deleting Ansible vault #{vault}", MU::NOTICE
    FileUtils.rm_rf(dir)
  end

end
encryptString(name, string) click to toggle source

Encrypt a string using +ansible-vault encrypt_string+ and print the the results to STDOUT. @param name [String]: The variable name to use for the string's YAML key @param string [String]: The string to encrypt

# File modules/mu/groomers/ansible.rb, line 440
def self.encryptString(name, string)
  pwfile = vaultPasswordFile
  cmd = %Q{#{ansibleExecDir}/ansible-vault}
  if !system(cmd, "encrypt_string", string, "--name", name, "--vault-password-file", pwfile)
    raise MuError, "Failed Ansible command: #{cmd} encrypt_string <redacted> --name #{name} --vault-password-file"
  end
  output
end
getSecret(vault: nil, item: nil, field: nil, deploy_dir: nil) click to toggle source

Retrieve sensitive data, which hopefully we're storing and retrieving in a secure fashion. @param vault [String]: A repository of secrets to search @param item [String]: The item within the repository to retrieve @param field [String]: OPTIONAL - A specific field within the item to return. @return [Hash]

# File modules/mu/groomers/ansible.rb, line 131
def self.getSecret(vault: nil, item: nil, field: nil, deploy_dir: nil)
  if vault.nil? or vault.empty?
    raise MuError, "Must call getSecret with at least a vault name"
  end
  pwfile = vaultPasswordFile

  dir = nil
  try = [secret_dir+"/"+vault]
  try << deploy_dir+"/ansible/vaults/"+vault if deploy_dir
  try << MU.mommacat.deploy_dir+"/ansible/vaults/"+vault if MU.mommacat.deploy_dir
  try.each { |maybe_dir|
    if Dir.exist?(maybe_dir) and (item.nil? or File.exist?(maybe_dir+"/"+item))
      dir = maybe_dir
      break
    end
  }
  if dir.nil?
    raise MuNoSuchSecret, "No such vault #{vault}"
  end

  data = nil
  if item
    itempath = dir+"/"+item
    if !File.exist?(itempath)
      raise MuNoSuchSecret, "No such item #{item} in vault #{vault}"
    end
    cmd = %Q{#{ansibleExecDir}/ansible-vault view #{itempath} --vault-password-file #{pwfile}}
    MU.log cmd
    a = `#{cmd}`
    # If we happen to have stored recognizeable JSON or YAML, return it
    # as parsed, which is a behavior we're used to from Chef vault.
    # Otherwise, return a String.
    begin
      data = JSON.parse(a)
    rescue JSON::ParserError
      begin
        data = YAML.load(a)
      rescue Psych::SyntaxError => e
        data = a
      end
    end
    [vault, item, field].each { |tier|
      if data and data.is_a?(Hash) and tier and data[tier]
        data = data[tier]
      end
    }
  else
    data = []
    Dir.foreach(dir) { |entry|
      next if entry == "." or entry == ".."
      next if File.directory?(dir+"/"+entry)
      data << entry
    }
  end

  data
end
listSecrets(user = MU.mu_user) click to toggle source

List the Ansible vaults, if any, owned by the specified Mu user @param user [String]: The user whose vaults we will list @return [Array<String>]

# File modules/mu/groomers/ansible.rb, line 425
def self.listSecrets(user = MU.mu_user)
  path = secret_dir(user)
  found = []
  Dir.foreach(path) { |entry|
    next if entry == "." or entry == ".."
    next if !File.directory?(path+"/"+entry)
    found << entry
  }
  found
end
new(node) click to toggle source

@param node [MU::Cloud::Server]: The server object on which we'll be operating

# File modules/mu/groomers/ansible.rb, line 39
def initialize(node)
  @config = node.config
  @server = node
  @inventory = Inventory.new(node.deploy)
  @mu_user = node.deploy.mu_user
  @ansible_path = node.deploy.deploy_dir+"/ansible"
  @ansible_execs = MU::Groomer::Ansible.ansibleExecDir

  if !MU::Groomer::Ansible.checkPythonDependencies(@server.windows?)
    raise AnsibleLibrariesError, "One or more python dependencies not available"
  end

  if !@ansible_execs or @ansible_execs.empty?
    raise NoAnsibleExecError, "No Ansible executables found in visible paths"
  end

  [@ansible_path, @ansible_path+"/roles", @ansible_path+"/vars", @ansible_path+"/group_vars", @ansible_path+"/vaults"].each { |dir|
    if !Dir.exist?(dir)
      MU.log "Creating #{dir}", MU::DEBUG
      Dir.mkdir(dir, 0755)
    end
  }
  MU::Groomer::Ansible.vaultPasswordFile(pwfile: "#{@ansible_path}/.vault_pw")
  installRoles
end
purge(node, _vaults_to_clean = [], noop = false) click to toggle source

Expunge Ansible resources associated with a node. @param node [String]: The Mu name of the node in question. @param _vaults_to_clean [Array<Hash>]: Dummy argument, part of this method's interface but not used by the Ansible layer @param noop [Boolean]: Skip actual deletion, just state what we'd do

# File modules/mu/groomers/ansible.rb, line 413
      def self.purge(node, _vaults_to_clean = [], noop = false)
        deploy = MU::MommaCat.new(MU.deploy_id)
        inventory = Inventory.new(deploy)
#        ansible_path = deploy.deploy_dir+"/ansible"
        if !noop
          inventory.remove(node)
        end
      end
pythonExecDir() click to toggle source

Hunt down and return a path for a Python executable @return [String]

# File modules/mu/groomers/ansible.rb, line 451
def self.pythonExecDir
  path = nil

  if File.exist?(BINDIR+"/python")
    path = BINDIR
  else
    paths = [ansibleExecDir]
    paths.concat(ENV['PATH'].split(/:/))
    paths << "/usr/bin" # not always in path, esp in pared-down Docker images
    paths.reject! { |p| p.nil? }
    paths.uniq.each { |bindir|
      if File.exist?(bindir+"/python")
        path = bindir
        break
      end
    }
  end
  path
end
saveSecret(vault: nil, item: nil, data: nil, permissions: false, deploy_dir: nil) click to toggle source

@param vault [String]: A repository of secrets to create/save into. @param item [String]: The item within the repository to create/save. @param data [Hash]: Data to save @param permissions [Boolean]: If true, save the secret under the current active deploy (if any), rather than in the global location for this user @param deploy_dir [String]: If permissions is true, save the secret here

# File modules/mu/groomers/ansible.rb, line 80
def self.saveSecret(vault: nil, item: nil, data: nil, permissions: false, deploy_dir: nil)

  if vault.nil? or vault.empty? or item.nil? or item.empty?
    raise MuError, "Must call saveSecret with vault and item names"
  end
  if vault.match(/\//) or item.match(/\//) #XXX this should just check for all valid dirname/filename chars
    raise MuError, "Ansible vault/item names cannot include forward slashes"
  end
  pwfile = vaultPasswordFile

  dir = if permissions
    if deploy_dir
      deploy_dir+"/ansible/vaults/"+vault
    elsif MU.mommacat
      MU.mommacat.deploy_dir+"/ansible/vaults/"+vault
    else
      raise "MU::Ansible::Groomer.saveSecret had permissions set to true, but I couldn't find an active deploy directory to save into"
    end
  else
    secret_dir+"/"+vault
  end
  path = dir+"/"+item

  if !Dir.exist?(dir)
    FileUtils.mkdir_p(dir, mode: 0700)
  end

  if File.exist?(path)
    MU.log "Overwriting existing vault #{vault} item #{item}"
  end

  File.open(path, File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
    f.write data.to_yaml
  }

  cmd = %Q{#{ansibleExecDir}/ansible-vault encrypt #{path} --vault-password-file #{pwfile}}
  MU.log cmd
  raise MuError, "Failed Ansible command: #{cmd}" if !system(cmd)
end
secret_dir(user = MU.mu_user) click to toggle source

Figure out where our main stash of secrets is, and make sure it exists @param user [String]: @return [String]

# File modules/mu/groomers/ansible.rb, line 540
def self.secret_dir(user = MU.mu_user)
  path = MU.dataDir(user) + "/ansible-secrets"
  Dir.mkdir(path, 0755) if !Dir.exist?(path)

  path
end
vaultPasswordFile(for_user = nil, pwfile: nil) click to toggle source

Get path to the .vault_pw file for the appropriate user. If it doesn't exist, generate it.

@param for_user [String]: @param pwfile [String] @return [String]

# File modules/mu/groomers/ansible.rb, line 524
def self.vaultPasswordFile(for_user = nil, pwfile: nil)
  pwfile ||= secret_dir(for_user)+"/.vault_pw"
  @@pwfile_semaphore.synchronize {
    if !File.exist?(pwfile)
      MU.log "Generating Ansible vault password file at #{pwfile}", MU::DEBUG
      File.open(pwfile, File::CREAT|File::RDWR|File::TRUNC, 0400) { |f|
        f.write Password.random(12..14)
      }
    end
  }
  pwfile
end

Public Instance Methods

bootstrap() click to toggle source

Bootstrap our server with Ansible- basically, just make sure this node is listed in our deployment's Ansible inventory.

# File modules/mu/groomers/ansible.rb, line 308
      def bootstrap
        @inventory.add(@server.config['name'], @server.windows? ? @server.canonicalIP : @server.mu_name)
        play = {
          "hosts" => @server.config['name']
        }

        if !@server.windows? and @server.config['ssh_user'] != "root"
          play["become"] = "yes"
        end

        if @server.config['run_list'] and !@server.config['run_list'].empty?
          play["roles"] = @server.config['run_list']
        end

        if @server.config['ansible_vars']
          play["vars"] = @server.config['ansible_vars']
        end

        if @server.windows?
          play["vars"] ||= {}
          play["vars"]["ansible_connection"] = "winrm"
          play["vars"]["ansible_winrm_scheme"] = "https"
          play["vars"]["ansible_winrm_transport"] = "ntlm"
          play["vars"]["ansible_winrm_server_cert_validation"] = "ignore" # XXX this sucks; use Mu_CA.pem if we can get it to work
#          play["vars"]["ansible_winrm_ca_trust_path"] = "#{MU.mySSLDir}/Mu_CA.pem"
          play["vars"]["ansible_user"] = @server.config['windows_admin_username']
          win_pw = @server.getWindowsAdminPassword

          pwfile = MU::Groomer::Ansible.vaultPasswordFile
          cmd = %Q{#{MU::Groomer::Ansible.ansibleExecDir}/ansible-vault}
          output = %x{#{cmd} encrypt_string '#{win_pw.gsub(/'/, "\\\\'")}' --vault-password-file #{pwfile}}

          play["vars"]["ansible_password"] = output
        end

        File.open(@ansible_path+"/"+@server.config['name']+".yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
          f.flock(File::LOCK_EX)
          f.puts [play].to_yaml.sub(/ansible_password: \|-?[\n\s]+/, 'ansible_password: ') # Ansible doesn't like this (legal) YAML
          f.flock(File::LOCK_UN)
        }
      end
deleteSecret(vault: nil, item: nil) click to toggle source

see {MU::Groomer::Ansible.deleteSecret}

# File modules/mu/groomers/ansible.rb, line 220
def deleteSecret(vault: nil, item: nil)
  self.class.deleteSecret(vault: vault, item: item)
end
getSecret(vault: nil, item: nil, field: nil) click to toggle source

see {MU::Groomer::Ansible.getSecret}

# File modules/mu/groomers/ansible.rb, line 190
def getSecret(vault: nil, item: nil, field: nil)
  self.class.getSecret(vault: vault, item: item, field: field, deploy_dir: @server.deploy.deploy_dir)
end
haveBootstrapped?() click to toggle source

Indicate whether our server has been bootstrapped with Ansible

# File modules/mu/groomers/ansible.rb, line 71
def haveBootstrapped?
  @inventory.haveNode?(@server.mu_name)
end
preClean(leave_ours = false) click to toggle source

This is a stub; since Ansible is effectively agentless, this operation doesn't have meaning.

# File modules/mu/groomers/ansible.rb, line 298
def preClean(leave_ours = false)
end
reinstall() click to toggle source

This is a stub; since Ansible is effectively agentless, this operation doesn't have meaning.

# File modules/mu/groomers/ansible.rb, line 303
def reinstall
end
run(purpose: "Ansible run", update_runlist: true, max_retries: 10, output: true, override_runlist: nil, reboot_first_fail: false, timeout: 1800) click to toggle source

Invoke the Ansible client on the node at the other end of a provided SSH session. @param purpose [String]: A string describing the purpose of this client run. @param max_retries [Integer]: The maximum number of attempts at a successful run to make before giving up. @param output [Boolean]: Display Ansible's regular (non-error) output to the console @param override_runlist [String]: Use the specified run list instead of the node's configured list

# File modules/mu/groomers/ansible.rb, line 230
def run(purpose: "Ansible run", update_runlist: true, max_retries: 10, output: true, override_runlist: nil, reboot_first_fail: false, timeout: 1800)
  bootstrap
  pwfile = MU::Groomer::Ansible.vaultPasswordFile
  stashHostSSLCertSecret

  ssh_user = @server.config['ssh_user'] || "root"

  if update_runlist
    bootstrap
  end

  tmpfile = nil
  playbook = if override_runlist and !override_runlist.empty?
    play = {
      "hosts" => @server.config['name']
    }
    if !@server.windows? and @server.config['ssh_user'] != "root"
      play["become"] = "yes"
    end
    play["roles"] = override_runlist if @server.config['run_list'] and !@server.config['run_list'].empty?
    play["vars"] = @server.config['ansible_vars'] if @server.config['ansible_vars']

    tmpfile = Tempfile.new("#{@server.config['name']}-override-runlist.yml")
    tmpfile.puts [play].to_yaml
    tmpfile.close
    tmpfile.path
  else
    "#{@server.config['name']}.yml"
  end

  cmd = %Q{cd #{@ansible_path} && echo "#{purpose}" && #{@ansible_execs}/ansible-playbook -i hosts #{playbook} --limit=#{@server.windows? ? @server.canonicalIP : @server.mu_name} --vault-password-file #{pwfile} --timeout=30 --vault-password-file #{@ansible_path}/.vault_pw -u #{ssh_user}}

  retries = 0
  begin
    MU.log cmd
    Timeout::timeout(timeout) {
      if output
        system("#{cmd}")
      else
        %x{#{cmd} 2>&1}
      end

      if $?.exitstatus != 0
        raise MU::Groomer::RunError, "Failed Ansible command: #{cmd}"
      end
    }
  rescue Timeout::Error, MU::Groomer::RunError => e
    if retries < max_retries
      if reboot_first_fail and e.class.name == "MU::Groomer::RunError"
        @server.reboot
        reboot_first_fail = false
      end
      sleep 30
      retries += 1
      MU.log "Failed Ansible run, will retry (#{retries.to_s}/#{max_retries.to_s})", MU::NOTICE, details: cmd

      retry
    else
      tmpfile.unlink if tmpfile
      raise MuError, "Failed Ansible command: #{cmd}"
    end
  end

  tmpfile.unlink if tmpfile
end
saveDeployData() click to toggle source

Synchronize the deployment structure managed by {MU::MommaCat} into some Ansible variables, so that nodes can access this metadata. @return [Hash]: The data synchronized.

# File modules/mu/groomers/ansible.rb, line 352
def saveDeployData
  @server.describe

  allvars = {
    "mu_deployment" => MU::Config.stripConfig(@server.deploy.deployment),
    "mu_service_name" => @config["name"],
    "mu_canonical_ip" => @server.canonicalIP,
    "mu_admin_email" => $MU_CFG['mu_admin_email'],
    "mu_environment" => MU.environment.downcase
  }
  allvars['mu_deployment']['ssh_public_key'] = @server.deploy.ssh_public_key

  if @server.config['cloud'] == "AWS"
    allvars["ec2"] = MU.structToHash(@server.cloud_desc, stringify_keys: true)
  end

  if @server.windows?
    allvars['windows_admin_username'] = @config['windows_admin_username']
  end

  if !@server.cloud.nil?
    allvars["cloudprovider"] = @server.cloud
  end

  File.open(@ansible_path+"/vars/main.yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
    f.flock(File::LOCK_EX)
    f.puts allvars.to_yaml
    f.flock(File::LOCK_UN)
  }

  groupvars = allvars.dup
  if @server.deploy.original_config.has_key?('parameters')
    groupvars["mu_parameters"] = @server.deploy.original_config['parameters']
  end
  if !@config['application_attributes'].nil?
    groupvars["application_attributes"] = @config['application_attributes']
  end
  if !@config['groomer_variables'].nil?
    groupvars["mu"] = @config['groomer_variables']
  end

  File.open(@ansible_path+"/group_vars/"+@server.config['name']+".yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
    f.flock(File::LOCK_EX)
    f.puts groupvars.to_yaml
    f.flock(File::LOCK_UN)
  }

  allvars['deployment']
end
saveSecret(vault: @server.mu_name, item: nil, data: nil, permissions: true) click to toggle source

see {MU::Groomer::Ansible.saveSecret}

# File modules/mu/groomers/ansible.rb, line 121
def saveSecret(vault: @server.mu_name, item: nil, data: nil, permissions: true)
  self.class.saveSecret(vault: vault, item: item, data: data, permissions: permissions, deploy_dir: @server.deploy.deploy_dir)
end

Private Instance Methods

installRoles() click to toggle source

Find all of the Ansible roles in the various configured Mu repositories and

# File modules/mu/groomers/ansible.rb, line 574
      def installRoles
        roledir = @ansible_path+"/roles"

        canon_links = {}

        repodirs = []

        # Make sure we search the global ansible_dir, if any is set
        if $MU_CFG and $MU_CFG['ansible_dir'] and !$MU_CFG['ansible_dir'].empty?
          if !Dir.exist?($MU_CFG['ansible_dir'])
            MU.log "Config lists an Ansible directory at #{$MU_CFG['ansible_dir']}, but I see no such directory", MU::WARN
          else
            repodirs << $MU_CFG['ansible_dir']
          end
        end

        # Hook up any Ansible roles listed in our platform repos
        if $MU_CFG and $MU_CFG['repos']
          $MU_CFG['repos'].each { |repo|
            repo.match(/\/([^\/]+?)(\.git)?$/)
            shortname = Regexp.last_match(1)
            repodirs << MU.dataDir + "/" + shortname
          }
        end

        repodirs.each { |repodir|
          ["roles", "ansible/roles"].each { |subdir|
            next if !Dir.exist?(repodir+"/"+subdir)
            Dir.foreach(repodir+"/"+subdir) { |role|
              next if [".", ".."].include?(role)
              realpath = repodir+"/"+subdir+"/"+role
              link = roledir+"/"+role
              
              if isAnsibleRole?(realpath)
                if !File.exist?(link)
                  File.symlink(realpath, link)
                  canon_links[role] = realpath
                elsif File.symlink?(link)
                  cur_target = File.readlink(link)
                  if cur_target == realpath
                    canon_links[role] = realpath
                  elsif !canon_links[role]
                    File.unlink(link)
                    File.symlink(realpath, link)
                    canon_links[role] = realpath
                  end
                end
              end
            }
          }
        }

        # Now layer on everything bundled in the main Mu repo
        Dir.foreach(MU.myRoot+"/ansible/roles") { |role|
          next if [".", ".."].include?(role)
          next if File.exist?(roledir+"/"+role)
          File.symlink(MU.myRoot+"/ansible/roles/"+role, roledir+"/"+role)
        }

        if @server.config['run_list']
          @server.config['run_list'].each { |role|
            found = false
            if !File.exist?(roledir+"/"+role)
              if role.match(/[^\.]\.[^\.]/) and @server.config['groomer_autofetch']
                system(%Q{#{@ansible_execs}/ansible-galaxy}, "--roles-path", roledir, "install", role)
                found = true
# XXX check return value
              else
                canon_links.keys.each { |longrole|
                  if longrole.match(/\.#{Regexp.quote(role)}$/)
                    File.symlink(roledir+"/"+longrole, roledir+"/"+role)
                    found = true
                    break
                  end
                }
              end
            else
              found = true
            end
            if !found
              raise MuError, "Unable to locate Ansible role #{role}"
            end
          }
        end
      end
isAnsibleRole?(path) click to toggle source

Make an effort to distinguish an Ansible role from other sorts of artifacts, since 'roles' is an awfully generic name for a directory. Short of a full, slow syntax check, this is the best we're liable to do.

# File modules/mu/groomers/ansible.rb, line 557
def isAnsibleRole?(path)
  begin
  Dir.foreach(path) { |entry|
    if File.directory?(path+"/"+entry) and
       ["tasks", "vars"].include?(entry)
      return true # https://knowyourmeme.com/memes/close-enough
    elsif ["metadata.rb", "recipes"].include?(entry)
      return false
    end
  }
  rescue Errno::ENOTDIR
  end
  false
end
secret_dir() click to toggle source

Figure out where our main stash of secrets is, and make sure it exists

# File modules/mu/groomers/ansible.rb, line 550
def secret_dir
  MU::Groomer::Ansible.secret_dir(@mu_user)
end
stashHostSSLCertSecret() click to toggle source

Upload the certificate to a Chef Vault for this node

# File modules/mu/groomers/ansible.rb, line 661
def stashHostSSLCertSecret
  cert, key = @server.deploy.nodeSSLCerts(@server)
  certdata = {
    "data" => {
      "node.crt" => cert.to_pem.chomp!.gsub(/\n/, "\\n"),
      "node.key" => key.to_pem.chomp!.gsub(/\n/, "\\n")
    }
  }
  saveSecret(item: "ssl_cert", data: certdata, permissions: true)

  saveSecret(item: "secrets", data: @config['secrets'], permissions: true) if !@config['secrets'].nil?
  certdata
end