seira 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|