linecook-gem 0.6.10 → 0.7.0
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 +4 -4
- data/lib/linecook-gem.rb +4 -6
- data/lib/linecook-gem/baker.rb +110 -0
- data/lib/linecook-gem/baker/docker.rb +114 -0
- data/lib/linecook-gem/cli.rb +63 -115
- data/lib/linecook-gem/config.rb +45 -0
- data/lib/linecook-gem/image.rb +77 -0
- data/lib/linecook-gem/image/crypt.rb +7 -40
- data/lib/linecook-gem/image/s3.rb +14 -14
- data/lib/linecook-gem/packager.rb +30 -0
- data/lib/linecook-gem/packager/packer.rb +249 -0
- data/lib/linecook-gem/packager/route53.rb +62 -0
- data/lib/linecook-gem/packager/squashfs.rb +51 -0
- data/lib/linecook-gem/util/downloader.rb +3 -42
- data/lib/linecook-gem/util/secrets.rb +47 -0
- data/lib/linecook-gem/version.rb +1 -1
- data/man/LINECOOK.1 +117 -86
- metadata +62 -110
- data/lib/linecook-gem/builder/build.rb +0 -44
- data/lib/linecook-gem/builder/darwin_backend.rb +0 -98
- data/lib/linecook-gem/builder/linux_backend.rb +0 -11
- data/lib/linecook-gem/builder/lxc.rb +0 -286
- data/lib/linecook-gem/builder/manager.rb +0 -79
- data/lib/linecook-gem/image/github.rb +0 -27
- data/lib/linecook-gem/image/manager.rb +0 -73
- data/lib/linecook-gem/packager/ebs.rb +0 -373
- data/lib/linecook-gem/packager/manager.rb +0 -23
- data/lib/linecook-gem/provisioner/chef-zero.rb +0 -149
- data/lib/linecook-gem/provisioner/manager.rb +0 -47
- data/lib/linecook-gem/provisioner/packer.rb +0 -82
- data/lib/linecook-gem/util/config.rb +0 -134
- data/lib/linecook-gem/util/executor.rb +0 -33
- data/lib/linecook-gem/util/ssh.rb +0 -190
@@ -1,47 +0,0 @@
|
|
1
|
-
require 'securerandom'
|
2
|
-
require 'fileutils'
|
3
|
-
|
4
|
-
require 'linecook-gem/builder/build'
|
5
|
-
require 'linecook-gem/provisioner/chef-zero'
|
6
|
-
require 'linecook-gem/provisioner/packer'
|
7
|
-
|
8
|
-
module Linecook
|
9
|
-
module Baker
|
10
|
-
extend self
|
11
|
-
|
12
|
-
def bake(name: nil, tag: nil, id: nil, snapshot: nil, upload: nil, package: nil, build: nil, keep: nil, clean: nil)
|
13
|
-
build_agent = Linecook::Build.new(name, tag: tag, id: id, image: image(name))
|
14
|
-
resume = clean ? false : true
|
15
|
-
provider(name).provision(build_agent, name) if build
|
16
|
-
snapshot = build_agent.snapshot(save: true, resume: resume) if snapshot || upload || package
|
17
|
-
Linecook::ImageManager.upload(snapshot, type: build_agent.type) if upload || package
|
18
|
-
Linecook::Packager.package(snapshot, type: build_agent.type, ami: true) if package
|
19
|
-
rescue => e
|
20
|
-
puts e.message
|
21
|
-
puts e.backtrace
|
22
|
-
raise e
|
23
|
-
ensure
|
24
|
-
build_agent.stop(clean: clean) unless keep
|
25
|
-
build_agent.clean if clean
|
26
|
-
FileUtils.rm_f(snapshot) if clean && File.exists?(snapshot.to_s)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def image(name)
|
32
|
-
Linecook.config[:roles][name.to_sym][:image]
|
33
|
-
end
|
34
|
-
|
35
|
-
def provider(name)
|
36
|
-
provisioner = Linecook.config[:roles][name.to_sym][:provisioner] || Linecook.config[:provisioner][:default_provider]
|
37
|
-
case provisioner
|
38
|
-
when :chefzero
|
39
|
-
Linecook::Chef
|
40
|
-
when :packer
|
41
|
-
Linecook::Packer
|
42
|
-
else
|
43
|
-
fail "Unsupported provisioner #{provisioner}"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,82 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'tempfile'
|
3
|
-
require 'mkmf'
|
4
|
-
require 'fileutils'
|
5
|
-
|
6
|
-
require 'linecook-gem/util/executor'
|
7
|
-
|
8
|
-
|
9
|
-
module Linecook
|
10
|
-
module Packer
|
11
|
-
def self.provision(build, role)
|
12
|
-
Runner.new(build, role).run
|
13
|
-
end
|
14
|
-
|
15
|
-
class Runner
|
16
|
-
SOURCE_URL = 'https://releases.hashicorp.com/packer/'
|
17
|
-
PACKER_VERSION = '0.8.6'
|
18
|
-
PACKER_PATH = File.join(Linecook::Config::LINECOOK_HOME, 'bin', 'packer')
|
19
|
-
|
20
|
-
include Executor
|
21
|
-
|
22
|
-
def initialize(build, role)
|
23
|
-
role_config = Linecook.config[:roles][role.to_sym]
|
24
|
-
@packer = packer_path
|
25
|
-
@build = build
|
26
|
-
@template = role_config[:template_path]
|
27
|
-
end
|
28
|
-
|
29
|
-
def run
|
30
|
-
@build.start
|
31
|
-
packer_template = inject_builder
|
32
|
-
Tempfile.open('packer-linecook') do |template|
|
33
|
-
template.write(JSON.dump(packer_template))
|
34
|
-
template.flush
|
35
|
-
execute("#{@packer} build #{template.path}", sudo: false)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def packer_path
|
42
|
-
found = File.exists?(PACKER_PATH) ? PACKER_PATH : find_executable('packer')
|
43
|
-
path = if found
|
44
|
-
version = execute("#{found} --version", sudo: false, capture: true)
|
45
|
-
Gem::Version.new(version) >= Gem::Version.new(PACKER_VERSION) ? found : nil
|
46
|
-
end
|
47
|
-
|
48
|
-
path ||= get_packer
|
49
|
-
end
|
50
|
-
|
51
|
-
def get_packer
|
52
|
-
puts "packer too old (<#{PACKER_VERSION}) or not present, getting latest packer"
|
53
|
-
arch = 1.size == 8 ? 'amd64' : '386'
|
54
|
-
path = File.join(File.dirname(PACKER_PATH), 'packer.zip')
|
55
|
-
url = File.join(SOURCE_URL, PACKER_VERSION, "packer_#{PACKER_VERSION}_#{Linecook::Config.platform}_#{arch}.zip")
|
56
|
-
Linecook::Downloader.download(url, path)
|
57
|
-
Linecook::Downloader.unzip(path)
|
58
|
-
PACKER_PATH
|
59
|
-
end
|
60
|
-
|
61
|
-
def inject_builder
|
62
|
-
packer_template = JSON.load(File.read(@template)).symbolize_keys
|
63
|
-
packer_template.merge(builders: null_builder)
|
64
|
-
end
|
65
|
-
|
66
|
-
def null_builder
|
67
|
-
[ communicator.merge(type: 'null') ]
|
68
|
-
end
|
69
|
-
|
70
|
-
def communicator
|
71
|
-
{
|
72
|
-
ssh_host: @build.ssh.hostname,
|
73
|
-
ssh_username: @build.ssh.username,
|
74
|
-
ssh_private_key_file: @build.ssh.keyfile,
|
75
|
-
ssh_bastion_host: Linecook::Builder.ssh.hostname,
|
76
|
-
ssh_bastion_username: Linecook::Builder.ssh.username,
|
77
|
-
ssh_bastion_private_key_file: Linecook::Builder.ssh.keyfile,
|
78
|
-
}
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,134 +0,0 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
require 'json'
|
3
|
-
require 'fileutils'
|
4
|
-
|
5
|
-
require 'xhyve'
|
6
|
-
|
7
|
-
module Linecook
|
8
|
-
def self.config
|
9
|
-
config = Config.load_config
|
10
|
-
config
|
11
|
-
end
|
12
|
-
|
13
|
-
module Config
|
14
|
-
extend self
|
15
|
-
attr_reader :config
|
16
|
-
|
17
|
-
LXC_MIN_VERSION = '1.1.4'
|
18
|
-
CONFIG_PATH = File.join(Dir.pwd, 'linecook.yml').freeze # File.expand_path('../../../config/config.yml', __FILE__)
|
19
|
-
SECRETS_PATH = File.join(Dir.pwd, 'secrets.ejson').freeze # File.expand_path('../../../config/config.yml', __FILE__)
|
20
|
-
LINECOOK_HOME = File.expand_path('~/.linecook').freeze
|
21
|
-
DEFAULT_CONFIG_PATH = File.join(LINECOOK_HOME, 'config.yml').freeze
|
22
|
-
DEFAULT_CONFIG = {
|
23
|
-
builder: {
|
24
|
-
image: :live_image,
|
25
|
-
name: 'builder',
|
26
|
-
home: '/u/lxc',
|
27
|
-
username: 'ubuntu',
|
28
|
-
password: 'ubuntu'
|
29
|
-
},
|
30
|
-
provisioner: {
|
31
|
-
default_provider: :chefzero,
|
32
|
-
default_image: :base_image,
|
33
|
-
chefzero: {
|
34
|
-
audit: true
|
35
|
-
}
|
36
|
-
},
|
37
|
-
image: {
|
38
|
-
provider: {
|
39
|
-
public: :github,
|
40
|
-
private: :s3,
|
41
|
-
},
|
42
|
-
images: {
|
43
|
-
live_iso: {
|
44
|
-
name: 'livesys.iso',
|
45
|
-
profile: :public,
|
46
|
-
},
|
47
|
-
live_image: {
|
48
|
-
name: 'livesys.squashfs',
|
49
|
-
profile: :public,
|
50
|
-
},
|
51
|
-
base_image: {
|
52
|
-
name: 'ubuntu-base.squashfs',
|
53
|
-
profile: :public,
|
54
|
-
}
|
55
|
-
}
|
56
|
-
},
|
57
|
-
packager: {
|
58
|
-
provider: :ebs,
|
59
|
-
ebs: {
|
60
|
-
hvm: true,
|
61
|
-
size: 10,
|
62
|
-
region: 'us-east-1',
|
63
|
-
copy_regions: [],
|
64
|
-
account_ids: []
|
65
|
-
}
|
66
|
-
},
|
67
|
-
roles: {
|
68
|
-
}
|
69
|
-
}
|
70
|
-
|
71
|
-
def secrets
|
72
|
-
@secrets ||= begin
|
73
|
-
secrets_path = ENV['LINECOOK_SECRETS_PATH'] || SECRETS_PATH
|
74
|
-
if File.exists?(secrets_path)
|
75
|
-
ejson_path = File.join(Gem::Specification.find_by_name('ejson').gem_dir, 'build', "#{Linecook::Config.platform}-amd64", 'ejson' )
|
76
|
-
command = "#{ejson_path} decrypt #{secrets_path}"
|
77
|
-
secrets = JSON.load(`sudo #{command}`)
|
78
|
-
secrets.deep_symbolize_keys
|
79
|
-
else
|
80
|
-
{}
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def load_config
|
86
|
-
@config ||= begin
|
87
|
-
config_path = ENV['LINECOOK_CONFIG_PATH'] || CONFIG_PATH
|
88
|
-
config = YAML.load(File.read(DEFAULT_CONFIG_PATH)) if File.exist?(DEFAULT_CONFIG_PATH)
|
89
|
-
config.deep_merge!(YAML.load(File.read(config_path))) if File.exist?(config_path)
|
90
|
-
(config || {}).deep_symbolize_keys!
|
91
|
-
config.deep_merge!(secrets)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def platform
|
96
|
-
case RbConfig::CONFIG['host_os'].downcase
|
97
|
-
when /linux/
|
98
|
-
'linux'
|
99
|
-
when /darwin/
|
100
|
-
'darwin'
|
101
|
-
else
|
102
|
-
fail 'Linux and OS X are the only supported systems'
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
private
|
107
|
-
|
108
|
-
def setup
|
109
|
-
FileUtils.mkdir_p(LINECOOK_HOME)
|
110
|
-
config = {}
|
111
|
-
config.merge!(YAML.load(File.read(DEFAULT_CONFIG_PATH))) if File.exist?(DEFAULT_CONFIG_PATH)
|
112
|
-
File.write(DEFAULT_CONFIG_PATH, YAML.dump(DEFAULT_CONFIG.deep_merge(config)))
|
113
|
-
check_perms if platform == 'darwin'
|
114
|
-
check_lxc if platform == 'linux'
|
115
|
-
end
|
116
|
-
|
117
|
-
def check_lxc
|
118
|
-
version = `lxc-info --version`
|
119
|
-
fail "lxc too old (<#{LXC_MIN_VERSION}) or not present" unless Gem::Version.new(version) >= Gem::Version.new(LXC_MIN_VERSION)
|
120
|
-
end
|
121
|
-
|
122
|
-
def check_perms
|
123
|
-
fix_perms if (File.stat(Xhyve::BINARY_PATH).uid != 0 || !File.stat(Xhyve::BINARY_PATH).setuid?)
|
124
|
-
end
|
125
|
-
|
126
|
-
def fix_perms
|
127
|
-
puts "Xhyve requires root until https://github.com/mist64/xhyve/issues/60 is resolved\nPlease enter your sudo password to setuid on the xhyve binary"
|
128
|
-
system("sudo chown root #{Xhyve::BINARY_PATH}")
|
129
|
-
system("sudo chmod +s #{Xhyve::BINARY_PATH}")
|
130
|
-
end
|
131
|
-
|
132
|
-
setup
|
133
|
-
end
|
134
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module Linecook
|
2
|
-
module Executor
|
3
|
-
def capture(command, sudo: true)
|
4
|
-
execute(command, sudo: sudo, capture: true)
|
5
|
-
end
|
6
|
-
|
7
|
-
def test(check)
|
8
|
-
if @remote
|
9
|
-
return @remote.test(check)
|
10
|
-
else
|
11
|
-
`#{check}`
|
12
|
-
return $?.exitstatus == 0
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def execute(command, sudo: true, capture: false)
|
17
|
-
command = "sudo #{command}" if sudo
|
18
|
-
if @remote
|
19
|
-
if capture
|
20
|
-
return @remote.capture(command)
|
21
|
-
else
|
22
|
-
@remote.run(command)
|
23
|
-
end
|
24
|
-
else
|
25
|
-
if capture
|
26
|
-
return `#{command}`
|
27
|
-
else
|
28
|
-
system(command)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,190 +0,0 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
|
3
|
-
require 'sshkit'
|
4
|
-
require 'sshkit/dsl'
|
5
|
-
require 'net/ssh'
|
6
|
-
require 'net/ssh/proxy/command'
|
7
|
-
|
8
|
-
require 'linecook-gem/util/config'
|
9
|
-
|
10
|
-
module Linecook
|
11
|
-
class SSHKit::Formatter::Linecook < SSHKit::Formatter::Pretty
|
12
|
-
def write_command(command)
|
13
|
-
log_command_start(command) unless command.started?
|
14
|
-
log_command_stdout(command) unless command.stdout.empty?
|
15
|
-
log_command_stderr(command) unless command.stderr.empty?
|
16
|
-
log_command_finished(command) if command.finished?
|
17
|
-
end
|
18
|
-
|
19
|
-
def log_command_start(command)
|
20
|
-
print(command, 'run'.colorize(:green), command.to_s.colorize(:yellow))
|
21
|
-
end
|
22
|
-
|
23
|
-
def log_command_stdout(command)
|
24
|
-
command.stdout.lines.each do |line|
|
25
|
-
print(command, 'out'.colorize(:green), line)
|
26
|
-
end
|
27
|
-
command.stdout = ''
|
28
|
-
end
|
29
|
-
|
30
|
-
def log_command_stderr(command)
|
31
|
-
command.stderr.lines.each do |line|
|
32
|
-
print(command, 'err'.colorize(:yellow), line)
|
33
|
-
end
|
34
|
-
command.stderr = ''
|
35
|
-
end
|
36
|
-
|
37
|
-
def log_command_finished(command)
|
38
|
-
if command.failure?
|
39
|
-
print(command, 'failed'.colorize(:red), "with status #{command.exit_status} #{command.to_s.colorize(:yellow)} in #{sprintf('%5.3f seconds', command.runtime)}")
|
40
|
-
else
|
41
|
-
print(command, 'done'.colorize(:green), "#{command.to_s.colorize(:yellow)} in #{sprintf('%5.3f seconds', command.runtime)}")
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def print(command, state, message)
|
46
|
-
line = "[#{command.host.to_s.colorize(:blue)}][#{state}] #{message}"
|
47
|
-
line << "\n" unless line.end_with?("\n")
|
48
|
-
original_output << line
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class SSH
|
53
|
-
MAX_RETRIES = 5
|
54
|
-
attr_reader :username, :password, :hostname, :keyfile
|
55
|
-
|
56
|
-
def self.private_key
|
57
|
-
userkey = File.expand_path("~/.ssh/id_rsa")
|
58
|
-
dedicated_key = File.join(Linecook::Config::LINECOOK_HOME, 'linecook_ssh.pem')
|
59
|
-
unless File.exists?(dedicated_key)
|
60
|
-
File.write(dedicated_key, SSHKey.generate.private_key)
|
61
|
-
FileUtils.chmod(0600, dedicated_key)
|
62
|
-
end
|
63
|
-
File.exists?(userkey) ? userkey : dedicated_key
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.public_key(keyfile: nil)
|
67
|
-
SSHKey.new(File.read(keyfile || private_key)).ssh_public_key
|
68
|
-
end
|
69
|
-
|
70
|
-
# Generate a fingerprint for an SSHv2 key used by amazon, yes, this is ugly
|
71
|
-
def self.sshv2_fingerprint(key)
|
72
|
-
_, blob = key.split(/ /)
|
73
|
-
blob = blob.unpack("m*").first
|
74
|
-
reader = Net::SSH::Buffer.new(blob)
|
75
|
-
k=reader.read_key
|
76
|
-
OpenSSL::Digest.new('md5',k.to_der).hexdigest.scan(/../).join(":")
|
77
|
-
end
|
78
|
-
|
79
|
-
def initialize(hostname, username: 'ubuntu', password: nil, keyfile: nil, proxy: nil, setup: true)
|
80
|
-
@username = username
|
81
|
-
@password = password
|
82
|
-
@hostname = hostname
|
83
|
-
@keyfile = keyfile
|
84
|
-
@proxy = proxy_command(proxy) if proxy
|
85
|
-
wait_for_connection
|
86
|
-
setup_ssh_key if @keyfile && setup
|
87
|
-
end
|
88
|
-
|
89
|
-
def forward(local, remote:nil)
|
90
|
-
remote ||= local
|
91
|
-
opts = { password: @password, paranoid: Net::SSH::Verifiers::Null.new }
|
92
|
-
opts.merge!({ proxy: @proxy }) if @proxy
|
93
|
-
@session = Net::SSH.start(@hostname, @username, opts)
|
94
|
-
@session.forward.remote(local, '127.0.0.1', remote)
|
95
|
-
# Block to ensure it's open
|
96
|
-
@session.loop { !@session.forward.active_remotes.include?([remote, '127.0.0.1']) }
|
97
|
-
@keep_forwarding = true
|
98
|
-
@forward = Thread.new do
|
99
|
-
@session.loop(0.1) { @keep_forwarding }
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def stop_forwarding
|
104
|
-
@keep_forwarding = false
|
105
|
-
@forward.join
|
106
|
-
@session.close unless @session.closed?
|
107
|
-
end
|
108
|
-
|
109
|
-
def test(check)
|
110
|
-
result = nil
|
111
|
-
on linecook_host do |_host|
|
112
|
-
result = test(check)
|
113
|
-
end
|
114
|
-
result
|
115
|
-
end
|
116
|
-
|
117
|
-
def run(command)
|
118
|
-
on linecook_host do |_host|
|
119
|
-
execute(command)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def capture(command)
|
124
|
-
output = nil
|
125
|
-
on linecook_host do |_host|
|
126
|
-
output = capture(command)
|
127
|
-
end
|
128
|
-
output
|
129
|
-
end
|
130
|
-
|
131
|
-
def upload(data, path)
|
132
|
-
on linecook_host do |_host|
|
133
|
-
contents = File.exist?(data) ? data : StringIO.new(data)
|
134
|
-
upload! contents, path
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def download(path, local: nil)
|
139
|
-
on linecook_host do |_host|
|
140
|
-
download! path, local || File.basename(path)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
private
|
145
|
-
|
146
|
-
def wait_for_connection
|
147
|
-
puts "Waiting for SSH connection"
|
148
|
-
attempts = 0
|
149
|
-
while attempts < MAX_RETRIES
|
150
|
-
begin
|
151
|
-
run("echo connected")
|
152
|
-
return
|
153
|
-
rescue SSHKit::Runner::ExecuteError
|
154
|
-
puts "Retrying SSH connection"
|
155
|
-
sleep(5)
|
156
|
-
attempts += 1
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
def setup_ssh_key
|
162
|
-
pubkey = Linecook::SSH.public_key(keyfile: @keyfile)
|
163
|
-
config = Linecook.config[:builder]
|
164
|
-
run("mkdir -p /home/#{config[:username]}/.ssh")
|
165
|
-
upload(pubkey, "/home/#{config[:username]}/.ssh/authorized_keys")
|
166
|
-
end
|
167
|
-
|
168
|
-
|
169
|
-
def linecook_host
|
170
|
-
@host ||= begin
|
171
|
-
host = SSHKit::Host.new(user: @username, hostname: @hostname)
|
172
|
-
host.password = @password if @password
|
173
|
-
opts = {paranoid: Net::SSH::Verifiers::Null.new}
|
174
|
-
opts.merge!({ proxy: @proxy }) if @proxy
|
175
|
-
opts.merge!({ keys: [@keyfile], auth_methods: %w(publickey password) }) if @keyfile
|
176
|
-
host.ssh_options = opts
|
177
|
-
host
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def proxy_command(proxy)
|
182
|
-
ssh_command = "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no #{"-i #{@keyfile}" if @keyfile} #{proxy.username}@#{proxy.hostname} nc %h %p"
|
183
|
-
|
184
|
-
|
185
|
-
Net::SSH::Proxy::Command.new(ssh_command)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
SSHKit.config.output = SSHKit::Formatter::Linecook.new($stdout)
|