Class: OpsWalrus::HostsFile

Inherits:
Object
  • Object
show all
Defined in:
lib/opswalrus/hosts_file.rb

Constant Summary collapse

DEFAULT_FILE_NAME =
"hosts.yaml"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hosts_file_path) ⇒ HostsFile

Returns a new instance of HostsFile.



33
34
35
36
37
# File 'lib/opswalrus/hosts_file.rb', line 33

def initialize(hosts_file_path)
  @hosts_file_path = File.absolute_path(hosts_file_path)
  @yaml = Psych.safe_load(File.read(hosts_file_path), permitted_classes: [SecretRef]) if File.exist?(hosts_file_path)
  @cipher = AgeEncryptionCipher.new(ids, App.instance.identity_file_paths)
end

Instance Attribute Details

#hosts_file_pathObject

Returns the value of attribute hosts_file_path.



30
31
32
# File 'lib/opswalrus/hosts_file.rb', line 30

def hosts_file_path
  @hosts_file_path
end

#yamlObject

Returns the value of attribute yaml.



31
32
33
# File 'lib/opswalrus/hosts_file.rb', line 31

def yaml
  @yaml
end

Class Method Details

.edit(file_path) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/opswalrus/hosts_file.rb', line 13

def self.edit(file_path)
  tempfile = Tempfile.create
  begin
    tempfile.close   # we want to close the file without unlinking so that the editor can write to it
    HostsFile.new(file_path).decrypt(tempfile.path)
    if TTY::Editor.open(tempfile.path)
      # tempfile.open()
      HostsFile.new(tempfile.path).encrypt(file_path)
    end
  ensure
    tempfile.close rescue nil
    File.unlink(tempfile)   # deletes the temp file
  end
end

Instance Method Details

#decrypt(decrypted_file_path = nil) ⇒ Object



178
179
180
181
182
183
184
# File 'lib/opswalrus/hosts_file.rb', line 178

def decrypt(decrypted_file_path = nil)
  decrypted_file_path ||= @hosts_file_path
  App.instance.debug "Decrypting #{@hosts_file_path} -> #{decrypted_file_path}."
  raise("Path to age identity not specified") if App.instance.identity_file_paths.empty?
  decrypt_secrets!
  File.write(decrypted_file_path, to_yaml)
end

#decrypt_secrets!Object



172
173
174
175
176
# File 'lib/opswalrus/hosts_file.rb', line 172

def decrypt_secrets!()
  secrets.each do |secret_name, secret|
    secret.decrypt(@cipher)
  end
end

#defaultsObject



39
40
41
# File 'lib/opswalrus/hosts_file.rb', line 39

def defaults
  @defaults ||= (@yaml["defaults"] || @yaml["default"] || {})
end

#encrypt(encrypted_file_path = nil) ⇒ Object



186
187
188
189
190
191
192
# File 'lib/opswalrus/hosts_file.rb', line 186

def encrypt(encrypted_file_path = nil)
  encrypted_file_path ||= @hosts_file_path
  App.instance.debug "Encrypting #{@hosts_file_path} -> #{encrypted_file_path}."
  raise("Path to age identity not specified") if App.instance.identity_file_paths.empty?
  encrypt_secrets!
  File.write(encrypted_file_path, to_yaml)
end

#encrypt_secrets!Object



166
167
168
169
170
# File 'lib/opswalrus/hosts_file.rb', line 166

def encrypt_secrets!()
  secrets.each do |secret_name, secret|
    secret.encrypt(@cipher)
  end
end

#envObject

returns a Hash object that may have nested structures



126
127
128
# File 'lib/opswalrus/hosts_file.rb', line 126

def env
  @env ||= (@yaml["env"] || {})
end

#has_secret?(secret_name) ⇒ Boolean

Returns:

  • (Boolean)


156
157
158
# File 'lib/opswalrus/hosts_file.rb', line 156

def has_secret?(secret_name)
  secrets[secret_name]
end

#hostsObject

returns an Array(Host)



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/opswalrus/hosts_file.rb', line 44

def hosts
  # @yaml is a map of the form:
  # {
  #   "198.23.249.13"=>{"hostname"=>"web1", "tags"=>["monopod", "racknerd", "vps", "2.5gb", "web1", "web", "ubuntu22.04"]},
  #   "107.175.91.150"=>{"tags"=>["monopod", "racknerd", "vps", "2.5gb", "pbx1", "pbx", "ubuntu22.04"]},
  #   "198.23.249.16"=>{"tags"=>["racknerd", "vps", "4gb", "kvm", "ubuntu20.04", "minecraft"]},
  #   "198.211.15.34"=>{"tags"=>["racknerd", "vps", "1.5gb", "kvm", "ubuntu20.04", "blog"]},
  #   "homeassistant.locallan.network"=>{"tags"=>["local", "homeassistant", "home", "rpi"]},
  #   "synology.locallan.network"=>{"tags"=>["local", "synology", "nas"]},
  #   "pfsense.locallan.network"=>false,
  #   "192.168.56.10"=>{"tags"=>["web", "vagrant"]}
  # }
  @yaml.map do |host_ref, host_attrs|
    next if ['default', 'defaults', 'env', 'ids', 'secrets'].include?(host_ref)

    host_params = host_attrs.is_a?(Hash) ? host_attrs : {}

    Host.new(host_ref, tags(host_ref), host_params, defaults, self)
  end.compact
end

#idsObject

returns a Hash of id-name/(PublicKey | Array String ) pairs



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/opswalrus/hosts_file.rb', line 106

def ids
  @ids ||= begin
    id_public_key_pairs, alias_id_set_pairs = (@yaml["ids"] || {}).partition{|k,v| String === v }.map(&:to_h)

    named_public_keys = id_public_key_pairs.map do |id_name, public_key_string|
      [id_name, PublicKey.new(id_name, public_key_string)]
    end.to_h

    # named_id_sets = alias_id_set_pairs.map do |id_name, id_array|
    #   referenced_public_keys = id_array.map {|id| named_public_keys[id] }.uniq.compact
    #   [id_name, referenced_public_keys]
    # end.to_h

    # named_public_keys.merge(named_id_sets)

    named_public_keys.merge(alias_id_set_pairs)
  end
end

#read_secret(secret_name) ⇒ Object

returns the decrypted value referenced by secret_name



161
162
163
164
# File 'lib/opswalrus/hosts_file.rb', line 161

def read_secret(secret_name)
  secret = secrets[secret_name]
  secret.decrypt(@cipher) if secret
end

#secretsObject

secrets are key/value pairs in which the key is an identifier used throughout the yaml file to reference the secret’s value and the associated value is either a Hash or a String.

  1. If the secret’s value is a Hash, then the Hash must consist of two keys - ids and a secret value:

    • the ids field explicitly names the intended audience for the secret value. The value associated with the ids field is either a String value structured as a comma delimited list of ids, in which each id is a reference to an id contained within the ids section of the inventory file OR the ids field is an Array value in which each element of the array is a reference to an id contained within the ids section of the inventory file.

    • the value field is a String value storing the secret value

  2. If the secret’s value is a String, then the string is the secret value, and is interpreted to be intended for use by an audience consisting of all of the ids listed in the ids section of the inventory file.

returns a Hash of secret-name/Secret pairs



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/opswalrus/hosts_file.rb', line 79

def secrets
  @secrets ||= (@yaml["secrets"] || {}).map do |secret_name, secret_attrs|
    audience_ids, secret_value = case secret_attrs
    when Hash
      id_names = case ids_value = secret_attrs["ids"]
      when String
        ids_value.split(',').map(&:strip)
      when Array
        ids_value.map {|elem| elem.to_s.strip }
      else
        raise "ids field beloning to secret '#{secret_name}' is of an unknown type: #{ids_value.class.name}: #{ids_value.inspect}"
      end
      value = secret_attrs["value"]
      [id_names, value]
    when String
      id_names = self.ids.select {|k,id_public_key_or_array_of_id_names| PublicKey === id_public_key_or_array_of_id_names }.keys
      value = secret_attrs
      [id_names, value]
    else
      raise "Secret '#{secret_name}' has an unexpected type #{secret_attrs.class.name}: #{secret_attrs.inspect}"
    end

    [secret_name, Secret.new(secret_name, secret_value, audience_ids)]
  end.to_h
end

#tags(host) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/opswalrus/hosts_file.rb', line 130

def tags(host)
  host_attrs = @yaml[host]

  case host_attrs
  when Array
    tags = host_attrs
    tags.compact.uniq
  when Hash
    tags = host_attrs["tags"] || []
    tags.compact.uniq
  end || []
end

#to_yamlObject



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/opswalrus/hosts_file.rb', line 143

def to_yaml
  hash = {}
  hash["defaults"] = defaults unless defaults.empty?
  hosts.each do |host|
    hash[host.host] = host.to_h
  end
  hash["secrets"] = secrets
  hash["ids"] = ids

  yaml = Psych.safe_dump(hash, permitted_classes: [SecretRef, Secret, PublicKey], line_width: 500)
  yaml.sub(/^---\s*/,"")    # omit the leading line: ---\n
end