seira 0.1.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 +7 -0
- data/.default-rubocop.yml +1614 -0
- data/.gitignore +12 -0
- data/.hound.yml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +109 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/seira +5 -0
- data/bin/setup +8 -0
- data/lib/seira.rb +125 -0
- data/lib/seira/app.rb +130 -0
- data/lib/seira/cluster.rb +79 -0
- data/lib/seira/memcached.rb +108 -0
- data/lib/seira/pods.rb +70 -0
- data/lib/seira/proxy.rb +13 -0
- data/lib/seira/random.rb +404 -0
- data/lib/seira/redis.rb +123 -0
- data/lib/seira/secrets.rb +148 -0
- data/lib/seira/settings.rb +59 -0
- data/lib/seira/setup.rb +99 -0
- data/lib/seira/version.rb +3 -0
- data/seira.gemspec +29 -0
- metadata +142 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# Example usages:
|
6
|
+
module Seira
|
7
|
+
class Cluster
|
8
|
+
VALID_ACTIONS = %w[bootstrap].freeze
|
9
|
+
|
10
|
+
attr_reader :action, :args, :context, :settings
|
11
|
+
|
12
|
+
def initialize(action:, args:, context:, settings:)
|
13
|
+
@action = action
|
14
|
+
@args = args
|
15
|
+
@context = context
|
16
|
+
@settings = settings
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
case action
|
21
|
+
when 'bootstrap'
|
22
|
+
run_bootstrap
|
23
|
+
else
|
24
|
+
fail "Unknown command encountered"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def switch(target_cluster:, verbose: false)
|
29
|
+
unless target_cluster && target_cluster != "" && settings.valid_cluster_names.include?(target_cluster)
|
30
|
+
puts "Please specify environment as first param to any seira command"
|
31
|
+
puts "Environment should be one of #{settings.valid_cluster_names}"
|
32
|
+
exit(1)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The context in kubectl are a bit more difficult to name. List by name only and search for the right one using a simple string match
|
36
|
+
cluster_metadata = settings.clusters[target_cluster]
|
37
|
+
|
38
|
+
puts("Switching to gcloud config of '#{target_cluster}' and kubernetes cluster of '#{cluster_metadata['cluster']}'") if verbose
|
39
|
+
exit(1) unless system("gcloud config configurations activate #{target_cluster}")
|
40
|
+
exit(1) unless system("kubectl config use-context #{cluster_metadata['cluster']}")
|
41
|
+
|
42
|
+
# If we haven't exited by now, it was successful
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.current_cluster
|
47
|
+
`kubectl config current-context`.chomp.strip
|
48
|
+
end
|
49
|
+
|
50
|
+
def current
|
51
|
+
puts `gcloud config get-value project`
|
52
|
+
puts `kubectl config current-context`
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Intended for use when spinning up a whole new cluster. It stores two main secrets
|
58
|
+
# in the default space that are intended to be copied into individual namespaces when
|
59
|
+
# new apps are built.
|
60
|
+
def run_bootstrap
|
61
|
+
dockercfg_location = args[0]
|
62
|
+
cloudsql_credentials_location = args[1]
|
63
|
+
|
64
|
+
if dockercfg_location.nil? || dockercfg_location == ''
|
65
|
+
puts 'Please specify the dockercfg json key location as first param.'
|
66
|
+
exit(1)
|
67
|
+
end
|
68
|
+
|
69
|
+
if cloudsql_credentials_location.nil? || cloudsql_credentials_location == ''
|
70
|
+
puts 'Please specify the cloudsql_credentials_location json key location as second param.'
|
71
|
+
exit(1)
|
72
|
+
end
|
73
|
+
|
74
|
+
# puts `kubectl create secret generic gcr-secret --namespace default --from-file=.dockercfg=#{dockercfg_location}`
|
75
|
+
puts `kubectl create secret docker-registry gcr-secret --docker-username=_json_key --docker-password="$(cat #{dockercfg_location})" --docker-server=https://gcr.io --docker-email=doesnotmatter@example.com`
|
76
|
+
puts `kubectl create secret generic cloudsql-credentials --namespace default --from-file=credentials.json=#{cloudsql_credentials_location}`
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Seira
|
5
|
+
class Memcached
|
6
|
+
VALID_ACTIONS = %w[list status credentials create delete].freeze
|
7
|
+
|
8
|
+
attr_reader :app, :action, :args, :context
|
9
|
+
|
10
|
+
def initialize(app:, action:, args:, context:)
|
11
|
+
@app = app
|
12
|
+
@action = action
|
13
|
+
@args = args
|
14
|
+
@context = context
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: logs, upgrades?, backups, restores, CLI connection
|
18
|
+
def run
|
19
|
+
case action
|
20
|
+
when 'list'
|
21
|
+
run_list
|
22
|
+
when 'status'
|
23
|
+
run_status
|
24
|
+
when 'credentials'
|
25
|
+
run_credentials
|
26
|
+
when 'create'
|
27
|
+
run_create
|
28
|
+
when 'delete'
|
29
|
+
run_delete
|
30
|
+
else
|
31
|
+
fail "Unknown command encountered"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def run_list
|
38
|
+
list = `helm list`.split("\n")
|
39
|
+
filtered_list = list.select { |item| item.start_with?("#{app}-memcached") }
|
40
|
+
filtered_list.each do |item|
|
41
|
+
puts item
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_status
|
46
|
+
puts `helm status #{app}-memcached-#{args[0]}`
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_create
|
50
|
+
# Fairly beefy default compute because it's cheap and the longer we can defer upgrading the
|
51
|
+
# better. Go even higher for production apps.
|
52
|
+
# TODO: Enable metrics
|
53
|
+
values = {
|
54
|
+
resources: {
|
55
|
+
requests: {
|
56
|
+
cpu: '1', # roughly 1 vCPU in both AWS and GCP terms
|
57
|
+
memory: '5Gi' # memcached is in-memory - give it a lot
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
args.each do |arg|
|
63
|
+
puts "Applying arg #{arg} to values"
|
64
|
+
if arg.start_with?('--memory=')
|
65
|
+
values[:resources][:requests][:memory] = arg.split('=')[1]
|
66
|
+
elsif arg.start_with?('--cpu=')
|
67
|
+
values[:resources][:requests][:cpu] = arg.split('=')[1]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
file_name = write_config(values)
|
72
|
+
unique_name = "#{Seira::Random.color}-#{Seira::Random.animal}"
|
73
|
+
name = "#{app}-memcached-#{unique_name}"
|
74
|
+
puts `helm install --namespace #{app} --name #{name} --wait -f #{file_name} stable/memcached`
|
75
|
+
|
76
|
+
File.delete(file_name)
|
77
|
+
|
78
|
+
puts "To get status: 'seira #{context[:cluster]} #{app} memcached status #{unique_name}'"
|
79
|
+
puts "To get credentials for storing in app secrets: 'siera #{context[:cluster]} #{app} memcached credentials #{unique_name}'"
|
80
|
+
puts "Service URI for this memcached instance: 'memcached://#{name}:11211'."
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_delete
|
84
|
+
to_delete = "#{app}-memcached-#{args[0]}"
|
85
|
+
|
86
|
+
exit(1) unless HighLine.agree("Are you sure you want to delete #{to_delete}? If any apps are using this memcached instance, they will break.")
|
87
|
+
|
88
|
+
if system("helm delete #{to_delete}")
|
89
|
+
puts "Successfully deleted #{to_delete}. Mistake and seeing errors now? You can rollback easily. Below is last 5 revisions of the now deleted resource."
|
90
|
+
history = `helm history --max 5 #{to_delete}`
|
91
|
+
puts history
|
92
|
+
last_revision = history.split("\n").last.split(" ").map(&:strip)[0]
|
93
|
+
puts "helm rollback #{to_delete} #{last_revision}"
|
94
|
+
puts "Docs: https://github.com/kubernetes/helm/blob/master/docs/helm/helm_rollback.md"
|
95
|
+
else
|
96
|
+
puts "Delete failed"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def write_config(values)
|
101
|
+
file_name = "tmp/temp-memcached-config-#{Seira::Cluster.current_cluster}-#{app}.json"
|
102
|
+
File.open(file_name, "wb") do |f|
|
103
|
+
f.write(values.to_json)
|
104
|
+
end
|
105
|
+
file_name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/seira/pods.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Seira
|
2
|
+
class Pods
|
3
|
+
VALID_ACTIONS = %w[list delete logs top run].freeze
|
4
|
+
|
5
|
+
attr_reader :app, :action, :key, :value, :context
|
6
|
+
|
7
|
+
def initialize(app:, action:, args:, context:)
|
8
|
+
@app = app
|
9
|
+
@action = action
|
10
|
+
@context = context
|
11
|
+
@key = args[0]
|
12
|
+
@value = args[1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
# TODO: Some options: 'top', 'kill', 'delete', 'logs'
|
17
|
+
case action
|
18
|
+
when 'list'
|
19
|
+
run_list
|
20
|
+
when 'delete'
|
21
|
+
run_delete
|
22
|
+
when 'logs'
|
23
|
+
run_logs
|
24
|
+
when 'top'
|
25
|
+
run_top
|
26
|
+
when 'run'
|
27
|
+
run_run
|
28
|
+
else
|
29
|
+
fail "Unknown command encountered"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def run_list
|
36
|
+
puts list_pods
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_delete
|
40
|
+
puts `kubectl delete pod #{@key} --namespace=#{@app}`
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_logs
|
44
|
+
puts `kubectl logs #{@key} --namespace=#{@app} -c #{@app}`
|
45
|
+
end
|
46
|
+
|
47
|
+
def run_top
|
48
|
+
puts `kubectl top pod #{@key} --namespace=#{@app} --containers`
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_run
|
52
|
+
pod_list = list_pods.split("\n")
|
53
|
+
target_pod_type = "#{@app}-web"
|
54
|
+
target_pod_options = pod_list.select { |pod| pod.include?(target_pod_type) }
|
55
|
+
|
56
|
+
if target_pod_options.count > 0
|
57
|
+
target_pod = target_pod_options[0]
|
58
|
+
pod_name = target_pod.split(" ")[0]
|
59
|
+
puts pod_name
|
60
|
+
system("kubectl exec -ti #{pod_name} --namespace=#{@app} -- bash")
|
61
|
+
else
|
62
|
+
puts "Could not find web with name #{target_pod_type} to attach to"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def list_pods
|
67
|
+
`kubectl get pods --namespace=#{@app} -o wide`
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/seira/proxy.rb
ADDED
data/lib/seira/random.rb
ADDED
@@ -0,0 +1,404 @@
|
|
1
|
+
# For random colors for resource installations via helm
|
2
|
+
module Seira
|
3
|
+
class Random
|
4
|
+
def self.color
|
5
|
+
%w[
|
6
|
+
red
|
7
|
+
green
|
8
|
+
blue
|
9
|
+
yellow
|
10
|
+
black
|
11
|
+
onyx
|
12
|
+
aqua
|
13
|
+
amber
|
14
|
+
violet
|
15
|
+
gray
|
16
|
+
tan
|
17
|
+
purple
|
18
|
+
white
|
19
|
+
pink
|
20
|
+
lime
|
21
|
+
orange
|
22
|
+
cherry
|
23
|
+
charcoal
|
24
|
+
coral
|
25
|
+
cyan
|
26
|
+
crimson
|
27
|
+
gold
|
28
|
+
silver
|
29
|
+
lemon
|
30
|
+
mustard
|
31
|
+
brown
|
32
|
+
tulip
|
33
|
+
].sample
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.animal
|
37
|
+
%w[
|
38
|
+
aardvark
|
39
|
+
abyssinian
|
40
|
+
affenpinscher
|
41
|
+
akbash
|
42
|
+
akita
|
43
|
+
albatross
|
44
|
+
alligator
|
45
|
+
alpaca
|
46
|
+
angelfish
|
47
|
+
ant
|
48
|
+
anteater
|
49
|
+
antelope
|
50
|
+
ape
|
51
|
+
armadillo
|
52
|
+
ass
|
53
|
+
avocet
|
54
|
+
axolotl
|
55
|
+
baboon
|
56
|
+
badger
|
57
|
+
balinese
|
58
|
+
bandicoot
|
59
|
+
barb
|
60
|
+
barnacle
|
61
|
+
barracuda
|
62
|
+
bat
|
63
|
+
beagle
|
64
|
+
bear
|
65
|
+
beaver
|
66
|
+
bee
|
67
|
+
beetle
|
68
|
+
binturong
|
69
|
+
bird
|
70
|
+
birman
|
71
|
+
bison
|
72
|
+
bloodhound
|
73
|
+
boar
|
74
|
+
bobcat
|
75
|
+
bombay
|
76
|
+
bongo
|
77
|
+
bonobo
|
78
|
+
booby
|
79
|
+
budgerigar
|
80
|
+
buffalo
|
81
|
+
bulldog
|
82
|
+
bullfrog
|
83
|
+
burmese
|
84
|
+
butterfly
|
85
|
+
caiman
|
86
|
+
camel
|
87
|
+
capybara
|
88
|
+
caracal
|
89
|
+
caribou
|
90
|
+
cassowary
|
91
|
+
cat
|
92
|
+
caterpillar
|
93
|
+
catfish
|
94
|
+
cattle
|
95
|
+
centipede
|
96
|
+
chameleon
|
97
|
+
chamois
|
98
|
+
cheetah
|
99
|
+
chicken
|
100
|
+
chihuahua
|
101
|
+
chimpanzee
|
102
|
+
chinchilla
|
103
|
+
chinook
|
104
|
+
chipmunk
|
105
|
+
chough
|
106
|
+
cichlid
|
107
|
+
clam
|
108
|
+
coati
|
109
|
+
cobra
|
110
|
+
cockroach
|
111
|
+
cod
|
112
|
+
collie
|
113
|
+
coral
|
114
|
+
cormorant
|
115
|
+
cougar
|
116
|
+
cow
|
117
|
+
coyote
|
118
|
+
crab
|
119
|
+
crane
|
120
|
+
crocodile
|
121
|
+
crow
|
122
|
+
curlew
|
123
|
+
cuscus
|
124
|
+
cuttlefish
|
125
|
+
dachshund
|
126
|
+
dalmatian
|
127
|
+
deer
|
128
|
+
dhole
|
129
|
+
dingo
|
130
|
+
dinosaur
|
131
|
+
discus
|
132
|
+
dodo
|
133
|
+
dog
|
134
|
+
dogfish
|
135
|
+
dolphin
|
136
|
+
donkey
|
137
|
+
dormouse
|
138
|
+
dotterel
|
139
|
+
dove
|
140
|
+
dragonfly
|
141
|
+
drever
|
142
|
+
duck
|
143
|
+
dugong
|
144
|
+
dunker
|
145
|
+
dunlin
|
146
|
+
eagle
|
147
|
+
earwig
|
148
|
+
echidna
|
149
|
+
eel
|
150
|
+
eland
|
151
|
+
elephant
|
152
|
+
elk
|
153
|
+
emu
|
154
|
+
falcon
|
155
|
+
ferret
|
156
|
+
finch
|
157
|
+
fish
|
158
|
+
flamingo
|
159
|
+
flounder
|
160
|
+
fly
|
161
|
+
fossa
|
162
|
+
fox
|
163
|
+
frigatebird
|
164
|
+
frog
|
165
|
+
galago
|
166
|
+
gar
|
167
|
+
gaur
|
168
|
+
gazelle
|
169
|
+
gecko
|
170
|
+
gerbil
|
171
|
+
gharial
|
172
|
+
gibbon
|
173
|
+
giraffe
|
174
|
+
gnat
|
175
|
+
gnu
|
176
|
+
goat
|
177
|
+
goldfinch
|
178
|
+
goldfish
|
179
|
+
goose
|
180
|
+
gopher
|
181
|
+
gorilla
|
182
|
+
goshawk
|
183
|
+
grasshopper
|
184
|
+
greyhound
|
185
|
+
grouse
|
186
|
+
guanaco
|
187
|
+
gull
|
188
|
+
guppy
|
189
|
+
hamster
|
190
|
+
hare
|
191
|
+
harrier
|
192
|
+
havanese
|
193
|
+
hawk
|
194
|
+
hedgehog
|
195
|
+
heron
|
196
|
+
herring
|
197
|
+
himalayan
|
198
|
+
hippopotamus
|
199
|
+
hornet
|
200
|
+
horse
|
201
|
+
human
|
202
|
+
hummingbird
|
203
|
+
hyena
|
204
|
+
ibis
|
205
|
+
iguana
|
206
|
+
impala
|
207
|
+
indri
|
208
|
+
insect
|
209
|
+
jackal
|
210
|
+
jaguar
|
211
|
+
javanese
|
212
|
+
jay
|
213
|
+
jellyfish
|
214
|
+
kakapo
|
215
|
+
kangaroo
|
216
|
+
kingfisher
|
217
|
+
kiwi
|
218
|
+
koala
|
219
|
+
kouprey
|
220
|
+
kudu
|
221
|
+
labradoodle
|
222
|
+
ladybird
|
223
|
+
lapwing
|
224
|
+
lark
|
225
|
+
lemming
|
226
|
+
lemur
|
227
|
+
leopard
|
228
|
+
liger
|
229
|
+
lion
|
230
|
+
lionfish
|
231
|
+
lizard
|
232
|
+
llama
|
233
|
+
lobster
|
234
|
+
locust
|
235
|
+
loris
|
236
|
+
louse
|
237
|
+
lynx
|
238
|
+
lyrebird
|
239
|
+
macaw
|
240
|
+
magpie
|
241
|
+
mallard
|
242
|
+
maltese
|
243
|
+
manatee
|
244
|
+
mandrill
|
245
|
+
markhor
|
246
|
+
marten
|
247
|
+
mastiff
|
248
|
+
mayfly
|
249
|
+
meerkat
|
250
|
+
millipede
|
251
|
+
mink
|
252
|
+
mole
|
253
|
+
molly
|
254
|
+
mongoose
|
255
|
+
mongrel
|
256
|
+
monkey
|
257
|
+
moorhen
|
258
|
+
moose
|
259
|
+
mosquito
|
260
|
+
moth
|
261
|
+
mouse
|
262
|
+
mule
|
263
|
+
narwhal
|
264
|
+
newt
|
265
|
+
nightingale
|
266
|
+
numbat
|
267
|
+
ocelot
|
268
|
+
octopus
|
269
|
+
okapi
|
270
|
+
olm
|
271
|
+
opossum
|
272
|
+
orang-utan
|
273
|
+
oryx
|
274
|
+
ostrich
|
275
|
+
otter
|
276
|
+
owl
|
277
|
+
ox
|
278
|
+
oyster
|
279
|
+
pademelon
|
280
|
+
panther
|
281
|
+
parrot
|
282
|
+
partridge
|
283
|
+
peacock
|
284
|
+
peafowl
|
285
|
+
pekingese
|
286
|
+
pelican
|
287
|
+
penguin
|
288
|
+
persian
|
289
|
+
pheasant
|
290
|
+
pig
|
291
|
+
pigeon
|
292
|
+
pika
|
293
|
+
pike
|
294
|
+
piranha
|
295
|
+
platypus
|
296
|
+
pointer
|
297
|
+
pony
|
298
|
+
poodle
|
299
|
+
porcupine
|
300
|
+
porpoise
|
301
|
+
possum
|
302
|
+
prawn
|
303
|
+
puffin
|
304
|
+
pug
|
305
|
+
puma
|
306
|
+
quail
|
307
|
+
quelea
|
308
|
+
quetzal
|
309
|
+
quokka
|
310
|
+
quoll
|
311
|
+
rabbit
|
312
|
+
raccoon
|
313
|
+
ragdoll
|
314
|
+
rail
|
315
|
+
ram
|
316
|
+
rat
|
317
|
+
rattlesnake
|
318
|
+
raven
|
319
|
+
reindeer
|
320
|
+
rhinoceros
|
321
|
+
robin
|
322
|
+
rook
|
323
|
+
rottweiler
|
324
|
+
ruff
|
325
|
+
salamander
|
326
|
+
salmon
|
327
|
+
sandpiper
|
328
|
+
saola
|
329
|
+
sardine
|
330
|
+
scorpion
|
331
|
+
seahorse
|
332
|
+
seal
|
333
|
+
serval
|
334
|
+
shark
|
335
|
+
sheep
|
336
|
+
shrew
|
337
|
+
shrimp
|
338
|
+
siamese
|
339
|
+
siberian
|
340
|
+
skunk
|
341
|
+
sloth
|
342
|
+
snail
|
343
|
+
snake
|
344
|
+
snowshoe
|
345
|
+
somali
|
346
|
+
sparrow
|
347
|
+
spider
|
348
|
+
sponge
|
349
|
+
squid
|
350
|
+
squirrel
|
351
|
+
starfish
|
352
|
+
starling
|
353
|
+
stingray
|
354
|
+
stinkbug
|
355
|
+
stoat
|
356
|
+
stork
|
357
|
+
swallow
|
358
|
+
swan
|
359
|
+
tang
|
360
|
+
tapir
|
361
|
+
tarsier
|
362
|
+
termite
|
363
|
+
tetra
|
364
|
+
tiffany
|
365
|
+
tiger
|
366
|
+
toad
|
367
|
+
tortoise
|
368
|
+
toucan
|
369
|
+
tropicbird
|
370
|
+
trout
|
371
|
+
tuatara
|
372
|
+
turkey
|
373
|
+
turtle
|
374
|
+
uakari
|
375
|
+
uguisu
|
376
|
+
umbrellabird
|
377
|
+
viper
|
378
|
+
vulture
|
379
|
+
wallaby
|
380
|
+
walrus
|
381
|
+
warthog
|
382
|
+
wasp
|
383
|
+
weasel
|
384
|
+
whale
|
385
|
+
whippet
|
386
|
+
wildebeest
|
387
|
+
wolf
|
388
|
+
wolverine
|
389
|
+
wombat
|
390
|
+
woodcock
|
391
|
+
woodlouse
|
392
|
+
woodpecker
|
393
|
+
worm
|
394
|
+
wrasse
|
395
|
+
wren
|
396
|
+
yak
|
397
|
+
zebra
|
398
|
+
zebu
|
399
|
+
zonkey
|
400
|
+
zorse
|
401
|
+
].sample
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|