talos 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6490791a8be853eea69f1bd18fdc18921ab76a62
4
- data.tar.gz: 62cc19aa2882f7ec979f381058fdbb2a72fbc7a9
3
+ metadata.gz: a963d1d621605ff8c6bbc9fcd5a66ec02f63509c
4
+ data.tar.gz: a16ebee4fbc780bf4911e6df1eeab33701f35e45
5
5
  SHA512:
6
- metadata.gz: b620929ce16fb6f872660a250b88f583a4b5ddd965a47b4b9d31372331f6a71c06b52cc0b6160b0677d5a6d1fcbca43e4c537cc3889791fd6a834831f8b18c51
7
- data.tar.gz: db70975dbeac2c5a8461b9761cfbaca65db63a0c68d4f4089e8cf52525cd7dd7497b2b6adecc1bb82436da2e2eaaa410fedebb6f66ac9c409b63ea2e0440c5c3
6
+ metadata.gz: 32b2795e08276905aec04ee7918da2041a612424772df39c65fe86ae961bea50cc44a3a53ab76fba03e2eed92bb7cc8dc84962dd79a9c3d55374b87ef6e023b4
7
+ data.tar.gz: c77f95c6f9c96a8cc25ba202548dda2be9d0495407106a307f380ce5edf484b1feb141dbb294981023011c4dcd447edabdf196df100baff91d8783f4bdf56303
@@ -0,0 +1,177 @@
1
+ Talos
2
+ =====
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/talos.svg)](http://badge.fury.io/rb/talos)
5
+ [![Build Status](https://travis-ci.org/spotify/talos.png?branch=master)](https://travis-ci.org/spotify/talos)
6
+
7
+ Talos is a rack application which servers Hiera yaml files over HTTP.
8
+ It authorizes clients based on the SSL certificates issued by the Puppet CA and returns only the files in the Hiera scope.
9
+
10
+ Talos is used to store and distribute secrets via Hiera to the masterless puppet clients.
11
+
12
+ How it works
13
+ ------------
14
+ Talos listens for incoming HTTP requests and returns compressed hiera
15
+ tree based on the client's SSL certificate.
16
+
17
+ To determine the list of files to send, Talos matches the certificate
18
+ common name against a list of regular expressions.
19
+
20
+ Fetching the tree
21
+ -----------------
22
+
23
+ It's possible to run a cron task or create a wrapper around the puppet
24
+ agent. Here's an example of the client-side code which uses local puppet SSL key
25
+ to authenticate:
26
+
27
+ ```ruby
28
+ require 'puppet'
29
+ Puppet[:confdir] = '/etc/puppetlabs/puppet/'
30
+ `/usr/bin/curl -s --fail -X GET -k https://talos.internal}/ \
31
+ --cert #{Puppet[:hostcert]} --key #{Puppet[:hostprivkey]} \
32
+ --data-urlencode pool=#{Facter.value(:pool)} > /etc/talos/tree.tar.gz`
33
+ `/bin/tar xzf /etc/talos/tree.tar.gz -C /etc/talos/hiera_secrets`
34
+ ```
35
+
36
+ In this example the client also passes `pool` variable which will
37
+ be included in the Hiera scope if `unsafe_scopes` option is enabled.
38
+
39
+ The received copy of the tree could be included in the local hiera config
40
+ and used in the normal puppet runs.
41
+
42
+ Configuration
43
+ -------------
44
+ Talos configuration is stored in `/etc/talos/talos.yaml`:
45
+
46
+ ```yaml
47
+ scopes:
48
+ # lon-puppet-a1: site = lon, role = puppet, pool = a
49
+ - match: '(?<site>[[:alpha:]]+)-(?<role>[a-z0-9]+)-(?<pool>[[:alpha:]]+)'
50
+ facts:
51
+ environment: production
52
+ - match: 'cloud\.example\.com'
53
+ facts:
54
+ environment: testing
55
+
56
+ unsafe_scopes: true
57
+ ```
58
+
59
+ When receiving a request, Talos iterates over `scopes` list and matches
60
+ the client certificate against the `match` blocks. If the match is
61
+ successful, Talos does 2 things:
62
+
63
+ 1. Adds all the named captures from the regexp to the Hiera scope
64
+ 2. Adds all the `facts` to the Hiera scope
65
+
66
+ Talos will iterate over all the regexps updating the
67
+ Hiera scope, meaning that the later matches will override the existing
68
+ scope on collision.
69
+
70
+ If `unsafe_scopes` option is enabled, Talos will also add all the parameters
71
+ passed by the client to the Hiera scope.
72
+
73
+ Hiera
74
+ -----
75
+ You need to provide `/etc/talos/hiera.yaml` file to configure Hiera
76
+ backend on the Talos server:
77
+
78
+ ```yaml
79
+ ---
80
+ :backends:
81
+ - yaml
82
+ :hierarchy:
83
+ - 'hiera-secrets/fqdn/%{fqdn}'
84
+ - 'hiera-secrets/role/%{role}/%{pod}/%{pool}'
85
+ - 'hiera-secrets/role/%{role}/%{pod}'
86
+ - 'hiera-secrets/role/%{role}'
87
+ - 'hiera-secrets/pod/%{pod}'
88
+ - 'hiera-secrets/common'
89
+ :yaml:
90
+ :datadir: '/etc/puppet'
91
+ :merge_behavior: :deeper
92
+ ```
93
+
94
+ Talos will use the `datadir` option to search for YAML files and it
95
+ will return only the files that match the Hiera scope of the clients.
96
+
97
+
98
+ Installing
99
+ ----------
100
+
101
+ First, install talos using rubygems:
102
+
103
+ $ gem install talos
104
+
105
+ Create a separate user and Document Root for the Rack application:
106
+
107
+ $ useradd talos --system --create-home --home-dir /var/lib/talos
108
+ $ mkdir -p /var/lib/talos/public /var/lib/talos/tmp /etc/talos
109
+ $ chown -R talos:talos /var/lib/talos/ /etc/talos
110
+
111
+ Then copy [config.ru](config.ru) to `/var/lib/talos/` directory.
112
+
113
+ You also need to copy and adjust [hiera.yaml](spec/fixtures/hiera.yaml) and
114
+ [talos.yaml](spec/fixtures/talos.yaml) configs in `/etc/talos` directory.
115
+
116
+ ### Hiera repository
117
+
118
+ You need to have a copy of the hiera-secrets repository available on the
119
+ talos server. Make sure it's located at the `datadir` specified in
120
+ `/etc/talos/hiera.yaml`
121
+
122
+ ### Apache
123
+
124
+ You can run Talos using Passenger or any other application server. Make
125
+ sure you use Puppet SSL keys to validate the client certificates and to
126
+ forward `SSL_CLIENT_S_DN_CN` header:
127
+
128
+ ```
129
+ <VirtualHost *:443>
130
+ DocumentRoot "/var/lib/talos/public"
131
+
132
+ <Directory "/var/lib/talos/public">
133
+ Require all granted
134
+ </Directory>
135
+
136
+ SSLEngine on
137
+ SSLCertificateFile "/etc/puppetlabs/puppet/ssl/certs/talos.internal.pem"
138
+ SSLCertificateKeyFile "/etc/puppetlabs/puppet/ssl/private_keys/talos.internal.pem"
139
+ SSLCertificateChainFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
140
+ SSLCACertificatePath "/etc/ssl/certs"
141
+ SSLCACertificateFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
142
+ SSLCARevocationFile "/etc/puppetlabs/puppet/ssl/crl.pem"
143
+ SSLVerifyClient require
144
+ SSLOptions +StdEnvVars +FakeBasicAuth
145
+ RequestHeader set SSL_CLIENT_S_DN_CN "%{SSL_CLIENT_S_DN_CN}s"
146
+ </VirtualHost>
147
+ ```
148
+
149
+ Contributing
150
+ ------------
151
+ 1. Fork the project on github
152
+ 2. Create your feature branch
153
+ 3. Open a Pull Request
154
+
155
+ This project adheres to the [Open Code of Conduct][code-of-conduct]. By
156
+ participating, you are expected to honor this code.
157
+
158
+ [code-of-conduct]:
159
+ https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md
160
+
161
+ License
162
+ -----------------
163
+ ```text
164
+ Copyright 2013-2016 Spotify AB
165
+
166
+ Licensed under the Apache License, Version 2.0 (the "License");
167
+ you may not use this file except in compliance with the License.
168
+ You may obtain a copy of the License at
169
+
170
+ http://www.apache.org/licenses/LICENSE-2.0
171
+
172
+ Unless required by applicable law or agreed to in writing, software
173
+ distributed under the License is distributed on an "AS IS" BASIS,
174
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
175
+ See the License for the specific language governing permissions and
176
+ limitations under the License.
177
+ ```
data/config.ru CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
2
  # uncomment if you wish to run from source code
3
- libdir = File.join(File.dirname(__FILE__), 'lib')
4
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+ # libdir = File.join(File.dirname(__FILE__), 'lib')
4
+ # $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
5
5
  require 'talos'
6
6
  run Talos
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
+ #--
3
+ # Copyright 2015 Spotify AB
4
+ #
5
+ # The contents of this file are licensed under the Apache License, Version 2.0
6
+ # (the "License"); you may not use this file except in compliance with the
7
+ # License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations under
15
+ # the License.
16
+ #++
17
+
2
18
  require 'sinatra/base'
3
19
  require 'json'
4
20
  require 'hiera'
@@ -8,91 +24,89 @@ require 'archive/tar/minitar'
8
24
  require 'pathname'
9
25
  include Archive::Tar
10
26
 
11
- def get_scope(fqdn, role = nil, pod = nil, pool = nil)
12
- # FQDN from ssl
13
- # role FQDN or client if unavailable
14
- # pod - FQDN or client
15
- # site - from pod
16
- # spenvironment - FQDN
17
- # pool - hostname or default to a
18
- scope = { 'fqdn' => fqdn }
19
- result = /(([[:alpha:]]+)\d*)-([a-z0-9]+)-([[:alpha:]])+/.match(fqdn)
20
-
21
- if !result.nil?
22
- scope['pod'] = result.captures[0]
23
- scope['site'] = result.captures[1]
24
- scope['role'] = result.captures[2]
25
- scope['pool'] = result.captures[3]
26
- scope['site'] = scope['pod'].tr('[0-9]', '')
27
- else
28
- # FQDN is not following our naming standard
29
- scope['pod'] = pod
30
- scope['role'] = role
31
- scope['pool'] = pool
32
- scope['site'] = scope['pod'].tr('[0-9]', '') unless pod.nil?
33
- end
34
-
35
- scope
36
- end
37
-
38
-
39
27
  class Talos < Sinatra::Base
40
- def absolute_datadir
41
- datadir = settings.hiera[:yaml][:datadir]
42
- datadir = File.join(File.dirname(__FILE__), '..', datadir) if Pathname.new(datadir).relative?
28
+ def self.prepare_config(path)
29
+ set :talos, YAML.load_file(path)
30
+ settings.talos['scopes'].each do |scope_config|
31
+ begin
32
+ scope_config['regexp'] = Regexp.new(scope_config['match'])
33
+ rescue
34
+ fail "Invalid regexp: #{scope_config['match']}"
35
+ end
36
+ end
43
37
  end
44
38
 
45
39
  configure :development do
46
40
  require 'sinatra/reloader'
47
41
  register Sinatra::Reloader
48
42
  set :hiera, Hiera::Config::load(File.expand_path('spec/fixtures/hiera.yaml'))
43
+ prepare_config('spec/fixtures/talos.yaml')
49
44
  set :show_exceptions, false
50
45
  end
46
+
51
47
  configure :production do
52
48
  set :hiera, Hiera::Config::load(File.expand_path('/etc/talos/hiera.yaml'))
49
+ prepare_config('/etc/talos/talos.yaml')
50
+ warn("SECURITY WARNING: unsafe_scopes are enabled, SSL authentication bypass is possible") if settings.talos['unsafe_scopes']
53
51
  end
54
52
 
55
- get '/' do
56
- fqdn = settings.development? ? params[:fqdn] : request.env['HTTP_SSL_CLIENT_S_DN_CN']
53
+ def absolute_datadir
54
+ datadir = settings.hiera[:yaml][:datadir]
55
+ datadir = File.join(File.dirname(__FILE__), '..', datadir) if Pathname.new(datadir).relative?
56
+ end
57
57
 
58
- # Get role/pod/pool from GET data(for hosts that don't follow our naming standard)
59
- unless params[:role].nil?
60
- role = params[:role].empty? ? nil : params[:role]
61
- end
62
- unless params[:pod].nil?
63
- pod = params[:pod].empty? ? nil : params[:pod]
64
- end
65
- unless params[:pool].nil?
66
- pool = params[:pool].empty? ? nil : params[:pool]
58
+ # Extracts scopes from FQDN using regexp with named captures
59
+ # Falls back to insecure arguments passed by the puppet agent
60
+ # (needed for the hosts not following naming convention)
61
+ def get_scope(fqdn)
62
+ scope = {'fqdn' => fqdn}
63
+ settings.talos['scopes'].each do | scope_config|
64
+ if m = fqdn.match(scope_config['regexp'])
65
+ scope.update(Hash[ m.names.zip( m.captures ) ])
66
+ scope.update(scope_config['facts'])
67
+ end
67
68
  end
68
69
 
69
- scope = get_scope(fqdn, role, pod, pool)
70
- files_to_pack = []
70
+ unsafe_scope = settings.talos['unsafe_scopes'] ? request.env['rack.request.query_hash'] : {}
71
+ scope.update(unsafe_scope)
72
+ # scope = {"pod"=>"lon3", "site"=>"lon", "role"=>"puppet", "pool"=>"a"}
73
+ scope
74
+ end
71
75
 
72
- # Get all yaml files matching scope
76
+ def files_in_scope(scope)
77
+ files = []
73
78
  Hiera::Backend.datasources(scope, nil) do |source, yamlfile|
74
79
  yamlfile = Hiera::Backend.datafile(:yaml, scope, source, 'yaml') || next
75
80
  next unless File.exist?(yamlfile)
76
81
  # Strip path from filename
77
- files_to_pack << yamlfile.gsub(settings.hiera[:yaml][:datadir] + '/', '')
82
+ files << yamlfile.gsub(settings.hiera[:yaml][:datadir] + '/', '')
78
83
  end
84
+ files
85
+ end
79
86
 
87
+ def compress_files(files)
80
88
  output = StringIO.new
81
89
  begin
82
90
  sgz = Zlib::GzipWriter.new(output)
83
91
  tar = Minitar::Output.new(sgz)
84
-
85
- Dir.chdir(absolute_datadir) do
86
- files_to_pack.each { |f| Minitar.pack_file(f, tar) }
87
- end
92
+ Dir.chdir(absolute_datadir) { files.each { |f| Minitar.pack_file(f, tar) } }
88
93
  ensure
89
94
  tar.close
90
95
  end
96
+ output
97
+ end
98
+
99
+ get '/' do
100
+ fqdn = settings.development? ? params[:fqdn] : request.env['HTTP_SSL_CLIENT_S_DN_CN']
101
+ scope = get_scope(fqdn)
102
+ files_to_pack = files_in_scope(scope)
103
+ archive = compress_files(files_to_pack)
91
104
  content_type 'application/x-gzip'
92
- output.string
105
+ archive.string
93
106
  end
94
107
 
95
108
  # Get the checksum the data folder symlink to
109
+ # Internal API
96
110
  get '/status' do
97
111
  begin
98
112
  File.readlink(absolute_datadir).split('.').last
@@ -4,15 +4,15 @@
4
4
 
5
5
  :hierarchy:
6
6
  - 'fqdn/%{fqdn}'
7
- - 'role/%{role}/%{spenvironment}/%{pool}'
7
+ - 'role/%{role}/%{environment}/%{pool}'
8
8
  - 'role/%{role}/%{pool}'
9
- - 'role/%{role}/%{spenvironment}'
9
+ - 'role/%{role}/%{environment}'
10
10
  - 'role/%{role}'
11
11
  - 'lsbdistcodename/%{lsbdistcodename}'
12
12
  - 'domain/%{domain}'
13
13
  - 'pod/%{pod}'
14
14
  - 'site/%{site}'
15
- - 'spenvironment/%{spenvironment}'
15
+ - 'environment/%{environment}'
16
16
  - common
17
17
 
18
18
  :yaml:
@@ -0,0 +1,10 @@
1
+ unsafe_scopes: true
2
+
3
+ scopes:
4
+ # lon3-puppet-a1: pod = lon3, site = lon3, role = puppet, pool = a
5
+ - match: '(?<pod>(?<site>[[:alpha:]]+)\d*)-(?<role>[a-z0-9]+)-(?<pool>[[:alpha:]]+)\d+'
6
+ facts:
7
+ environment: production
8
+ - match: 'cloud\.example\.com'
9
+ facts:
10
+ environment: testing
@@ -5,26 +5,30 @@ describe 'talos' do
5
5
 
6
6
  def match_query_to_files(query, files)
7
7
  get query
8
- last_response.should be_ok
9
- file = Tempfile.new('spec')
10
- file.write(last_response.body)
11
- file.flush
12
- output = `tar -tf #{file.path}`
13
- file.unlink
14
- files.each { |f| expect(output).to match(f) }
8
+ expect(last_response).to be_ok
9
+ Tempfile.open('spec') do |file|
10
+ file.write(last_response.body)
11
+ file.flush
12
+ files_in_archive = `tar -tf #{file.path}`.split
13
+ files.each { |f| expect(files.sort).to eq(files_in_archive.sort) }
14
+ end
15
15
  end
16
16
 
17
- it 'should return common.yaml' do
17
+ it 'should detect scope and return YAML files' do
18
18
  { '/?fqdn=testing.int.sto.example.com' =>
19
- ['common.yaml'],
19
+ %w(common.yaml),
20
20
  '/?role=puppet&pod=sto3&fqdn=sto3-puppet-a1.sto3.example.com' =>
21
- ['common.yaml', 'role/puppet.yaml'],
21
+ %w(common.yaml role/puppet.yaml),
22
22
  '/?fqdn=sto3-puppet-a1.sto3.example.com' =>
23
- ['common.yaml', 'role/puppet.yaml'],
23
+ %w(common.yaml role/puppet.yaml),
24
24
  '/?role=puppet&pod=sto3&fqdn=foo.bar' =>
25
25
  %w(common.yaml role/puppet.yaml fqdn/foo.bar.yaml),
26
26
  '/?fqdn=something.random&role=foobar&pool=z' =>
27
- %w(common.yaml role/foobar/z.yaml)
27
+ %w(common.yaml role/foobar/z.yaml),
28
+ '/?fqdn=sjc1-puppet-a1' =>
29
+ %w(common.yaml role/puppet.yaml site/sjc.yaml),
30
+ '/?fqdn=sjc1-foobar-a1.cloud.example.com' =>
31
+ %w(common.yaml site/sjc.yaml role/foobar/testing.yaml),
28
32
  }.each do |query, files|
29
33
  match_query_to_files(query, files)
30
34
  end
@@ -32,7 +36,7 @@ describe 'talos' do
32
36
 
33
37
  it 'should resturn the checksum master symlink to' do
34
38
  get '/status'
35
- last_response.should be_ok
36
- last_response.body.should match('3fa3fd97848a72ae539b75bccd6028cd1d4e92e3')
39
+ expect(last_response).to be_ok
40
+ expect(last_response.body).to match('3fa3fd97848a72ae539b75bccd6028cd1d4e92e3')
37
41
  end
38
42
  end
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
- s.version = '0.1.0'
2
+ s.version = '0.1.1'
3
3
  s.name = 'talos'
4
4
  s.authors = ['Alexey Lapitsky', 'Johan Haals']
5
5
  s.email = 'alexey@spotify.com'
6
- s.summary = %q{Distribute compressed hiera yaml files over HTTP}
7
- s.description = %q{Serve hiera data over HTTP}
6
+ s.summary = %q{Hiera secrets distribution over HTTP}
7
+ s.description = %q{Distribute compressed hiera yaml files to authenticated puppet clients over HTTP}
8
8
  s.homepage = 'https://github.com/spotify/talos'
9
9
  s.license = 'Apache 2.0'
10
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: talos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Lapitsky
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-12-17 00:00:00.000000000 Z
12
+ date: 2016-01-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -109,7 +109,8 @@ dependencies:
109
109
  - - '>='
110
110
  - !ruby/object:Gem::Version
111
111
  version: '2.9'
112
- description: Serve hiera data over HTTP
112
+ description: Distribute compressed hiera yaml files to authenticated puppet clients
113
+ over HTTP
113
114
  email: alexey@spotify.com
114
115
  executables: []
115
116
  extensions: []
@@ -119,14 +120,18 @@ files:
119
120
  - .rspec
120
121
  - Gemfile
121
122
  - LICENSE
123
+ - README.md
122
124
  - Rakefile
123
125
  - config.ru
124
126
  - lib/talos.rb
125
127
  - spec/fixtures/hiera.yaml
126
128
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/common.yaml
127
129
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/fqdn/foo.bar.yaml
130
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/testing.yaml
128
131
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/z.yaml
129
132
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/puppet.yaml
133
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/site/sjc.yaml
134
+ - spec/fixtures/talos.yaml
130
135
  - spec/spec_helper.rb
131
136
  - spec/talos_spec.rb
132
137
  - talos.gemspec
@@ -153,12 +158,15 @@ rubyforge_project:
153
158
  rubygems_version: 2.0.14
154
159
  signing_key:
155
160
  specification_version: 4
156
- summary: Distribute compressed hiera yaml files over HTTP
161
+ summary: Hiera secrets distribution over HTTP
157
162
  test_files:
158
163
  - spec/fixtures/hiera.yaml
159
164
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/common.yaml
160
165
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/fqdn/foo.bar.yaml
166
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/testing.yaml
161
167
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/z.yaml
162
168
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/puppet.yaml
169
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/site/sjc.yaml
170
+ - spec/fixtures/talos.yaml
163
171
  - spec/spec_helper.rb
164
172
  - spec/talos_spec.rb