deltacloud-core 0.1.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/DISCLAIMER +8 -0
- data/{COPYING → LICENSE} +0 -0
- data/NOTICE +13 -0
- data/Rakefile +50 -51
- data/bin/deltacloudd +8 -1
- data/config.ru +0 -2
- data/config/drivers.yaml +48 -0
- data/deltacloud-core.gemspec +75 -0
- data/deltacloud.rb +3 -2
- data/lib/deltacloud/backend_capability.rb +15 -3
- data/lib/deltacloud/base_driver.rb +0 -2
- data/lib/deltacloud/base_driver/base_driver.rb +85 -89
- data/lib/deltacloud/base_driver/features.rb +61 -7
- data/lib/deltacloud/base_driver/mock_driver.rb +42 -43
- data/lib/deltacloud/core_ext.rb +18 -0
- data/lib/deltacloud/core_ext/integer.rb +31 -0
- data/lib/deltacloud/core_ext/string.rb +50 -0
- data/lib/deltacloud/drivers/azure/azure_driver.rb +71 -22
- data/lib/deltacloud/drivers/ec2/ec2_driver.rb +641 -584
- data/lib/deltacloud/drivers/ec2/ec2_mock_driver.rb +0 -2
- data/lib/deltacloud/drivers/eucalyptus/eucalyptus_driver.rb +167 -0
- data/lib/deltacloud/drivers/gogrid/gogrid_client.rb +39 -1
- data/lib/deltacloud/drivers/gogrid/gogrid_driver.rb +41 -25
- data/lib/deltacloud/drivers/mock/data/buckets/blobs/blob1.yml +6 -3
- data/lib/deltacloud/drivers/mock/data/buckets/blobs/blob2.yml +6 -3
- data/lib/deltacloud/drivers/mock/data/buckets/blobs/blob3.yml +4 -2
- data/lib/deltacloud/drivers/mock/data/buckets/blobs/blob4.yml +5 -2
- data/lib/deltacloud/drivers/mock/data/buckets/blobs/blob5.yml +4 -2
- data/lib/deltacloud/drivers/mock/data/instances/inst0.yml +1 -0
- data/lib/deltacloud/drivers/mock/data/instances/inst1.yml +1 -0
- data/lib/deltacloud/drivers/mock/data/instances/inst2.yml +1 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol1.yml +1 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol2.yml +1 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol3.yml +1 -0
- data/lib/deltacloud/drivers/mock/mock_driver.rb +138 -30
- data/lib/deltacloud/drivers/opennebula/cloud_client.rb +13 -15
- data/lib/deltacloud/drivers/opennebula/occi_client.rb +13 -15
- data/lib/deltacloud/drivers/opennebula/opennebula_driver.rb +13 -15
- data/lib/deltacloud/drivers/rackspace/rackspace_driver.rb +224 -113
- data/lib/deltacloud/drivers/rhevm/rhevm_client.rb +332 -0
- data/lib/deltacloud/drivers/rhevm/rhevm_driver.rb +221 -170
- data/lib/deltacloud/drivers/rimuhosting/rimuhosting_driver.rb +0 -1
- data/lib/deltacloud/drivers/sbc/sbc_client.rb +247 -0
- data/lib/deltacloud/drivers/sbc/sbc_driver.rb +297 -0
- data/lib/deltacloud/drivers/terremark/terremark_driver.rb +0 -2
- data/lib/deltacloud/hardware_profile.rb +1 -3
- data/lib/deltacloud/helpers.rb +0 -2
- data/lib/deltacloud/helpers/application_helper.rb +86 -12
- data/lib/deltacloud/helpers/blob_stream.rb +19 -2
- data/lib/deltacloud/helpers/conversion_helper.rb +0 -2
- data/lib/deltacloud/helpers/hardware_profiles_helper.rb +0 -2
- data/lib/deltacloud/method_serializer.rb +0 -2
- data/lib/deltacloud/models/base_model.rb +0 -2
- data/lib/deltacloud/models/blob.rb +1 -2
- data/lib/deltacloud/models/bucket.rb +0 -2
- data/lib/deltacloud/models/image.rb +0 -2
- data/lib/deltacloud/models/instance.rb +19 -2
- data/lib/deltacloud/models/instance_profile.rb +4 -2
- data/lib/deltacloud/models/key.rb +0 -2
- data/lib/deltacloud/models/load_balancer.rb +0 -2
- data/lib/deltacloud/models/realm.rb +0 -2
- data/lib/deltacloud/models/storage_snapshot.rb +0 -2
- data/lib/deltacloud/models/storage_volume.rb +4 -2
- data/lib/deltacloud/runner.rb +132 -0
- data/lib/deltacloud/state_machine.rb +0 -2
- data/lib/deltacloud/validation.rb +9 -7
- data/lib/drivers.rb +36 -48
- data/lib/sinatra/accept_media_types.rb +26 -0
- data/lib/sinatra/lazy_auth.rb +16 -0
- data/lib/sinatra/rabbit.rb +112 -54
- data/lib/sinatra/rack_driver_select.rb +50 -16
- data/lib/sinatra/rack_etag.rb +79 -0
- data/lib/sinatra/rack_matrix_params.rb +84 -0
- data/lib/sinatra/rack_runtime.rb +47 -0
- data/lib/sinatra/static_assets.rb +16 -0
- data/lib/sinatra/url_for.rb +31 -4
- data/public/favicon.ico +0 -0
- data/public/images/bread-bg.png +0 -0
- data/public/images/error.png +0 -0
- data/public/images/pending.png +0 -0
- data/public/images/running.png +0 -0
- data/public/images/stopped.png +0 -0
- data/public/javascripts/application.js +35 -0
- data/public/stylesheets/compiled/application.css +59 -5
- data/public/stylesheets/compiled/screen.css +1 -1
- data/server.rb +293 -29
- data/support/fedora/deltacloud-core +78 -0
- data/support/fedora/deltacloud-core.spec +143 -0
- data/support/fedora/deltacloudd +78 -18
- data/support/fedora/rubygem-deltacloud-core.spec +76 -40
- data/tests/common.rb +172 -0
- data/tests/drivers/mock/api_test.rb +133 -0
- data/tests/drivers/mock/hardware_profiles_test.rb +134 -0
- data/tests/drivers/mock/images_test.rb +126 -0
- data/tests/drivers/mock/instance_states_test.rb +71 -0
- data/tests/drivers/mock/instances_test.rb +236 -0
- data/tests/drivers/mock/realms_test.rb +93 -0
- data/tests/drivers/mock/setup.rb +3 -0
- data/tests/drivers/mock/url_for_test.rb +67 -0
- data/tests/drivers/rackspace/api_test.rb +41 -0
- data/tests/drivers/rackspace/hardware_profiles_test.rb +53 -0
- data/tests/drivers/rackspace/images_test.rb +40 -0
- data/tests/drivers/rackspace/instances_test.rb +161 -0
- data/tests/drivers/rackspace/realms_test.rb +36 -0
- data/tests/drivers/rackspace/setup.rb +14 -0
- data/tests/drivers/rhevm/api_test.rb +39 -0
- data/tests/drivers/rhevm/hardware_profiles_test.rb +53 -0
- data/tests/drivers/rhevm/images_test.rb +42 -0
- data/tests/drivers/rhevm/instances_test.rb +179 -0
- data/tests/drivers/rhevm/realms_test.rb +35 -0
- data/tests/drivers/rhevm/setup.rb +14 -0
- data/tests/rabbit_test.rb +52 -0
- data/views/api/show.html.haml +2 -5
- data/views/blobs/new.html.haml +17 -1
- data/views/blobs/show.html.haml +6 -0
- data/views/blobs/show.xml.haml +5 -1
- data/views/buckets/index.html.haml +1 -12
- data/views/buckets/index.xml.haml +3 -5
- data/views/docs/operation.html.haml +23 -11
- data/views/drivers/index.html.haml +15 -0
- data/views/drivers/index.xml.haml +7 -0
- data/views/drivers/show.html.haml +20 -0
- data/views/drivers/show.xml.haml +7 -0
- data/views/error.html.haml +31 -0
- data/views/errors/auth_exception.xml.haml +2 -1
- data/views/errors/backend_capability_failure.xml.haml +2 -1
- data/views/errors/backend_error.html.haml +3 -0
- data/views/errors/backend_error.xml.haml +2 -2
- data/views/errors/validation_failure.xml.haml +3 -2
- data/views/images/index.html.haml +1 -6
- data/views/images/index.xml.haml +2 -0
- data/views/images/new.html.haml +14 -0
- data/views/images/show.xml.haml +2 -0
- data/views/instances/index.html.haml +8 -6
- data/views/instances/index.xml.haml +4 -0
- data/views/instances/new.html.haml +40 -11
- data/views/instances/run.html.haml +9 -0
- data/views/instances/run.xml.haml +7 -0
- data/views/instances/run_command.html.haml +16 -0
- data/views/instances/show.html.haml +14 -0
- data/views/instances/show.xml.haml +12 -4
- data/views/layout.html.haml +7 -2
- data/views/load_balancers/index.html.haml +1 -1
- data/views/load_balancers/new.html.haml +2 -2
- data/views/load_balancers/show.html.haml +1 -1
- data/views/storage_snapshots/index.html.haml +3 -0
- data/views/storage_snapshots/new.html.haml +9 -0
- data/views/storage_volumes/attach.html.haml +20 -0
- data/views/storage_volumes/index.html.haml +16 -1
- data/views/storage_volumes/index.xml.haml +1 -10
- data/views/storage_volumes/new.html.haml +17 -0
- data/views/storage_volumes/show.html.haml +8 -0
- data/views/storage_volumes/show.xml.haml +25 -3
- metadata +197 -127
- data/lib/deltacloud/drivers/rackspace/rackspace_client.rb +0 -130
- data/parse.rb +0 -7
- data/test.rb +0 -3
- data/views/api/drivers.xml.haml +0 -6
@@ -1,6 +1,4 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (C) 2009, 2010 Red Hat, Inc.
|
3
|
-
#
|
4
2
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
5
3
|
# contributor license agreements. See the NOTICE file distributed with
|
6
4
|
# this work for additional information regarding copyright ownership. The
|
@@ -25,8 +23,15 @@ module Deltacloud
|
|
25
23
|
module Mock
|
26
24
|
class MockDriver < Deltacloud::BaseDriver
|
27
25
|
|
26
|
+
# If the provider is set to storage, pretend to be a storage-only
|
27
|
+
# driver
|
28
28
|
def supported_collections
|
29
|
-
|
29
|
+
endpoint = Thread.current[:provider] || ENV['API_PROVIDER']
|
30
|
+
if endpoint == 'storage'
|
31
|
+
[:buckets]
|
32
|
+
else
|
33
|
+
DEFAULT_COLLECTIONS + [:buckets, :keys]
|
34
|
+
end
|
30
35
|
end
|
31
36
|
|
32
37
|
( REALMS = [
|
@@ -128,6 +133,33 @@ class MockDriver < Deltacloud::BaseDriver
|
|
128
133
|
images.sort_by{|e| [e.owner_id,e.description]}
|
129
134
|
end
|
130
135
|
|
136
|
+
def create_image(credentials, opts={})
|
137
|
+
check_credentials(credentials)
|
138
|
+
instance = instance(credentials, :id => opts[:instance_id])
|
139
|
+
raise BackendError::new(500, 'CreateImageNotSupported', '', '') unless instance.can_create_image?
|
140
|
+
ids = Dir[ "#{@storage_root}/images/*.yml" ].collect{|e| File.basename( e, ".yml" )}
|
141
|
+
count = 0
|
142
|
+
while true
|
143
|
+
next_id = "img#{count}"
|
144
|
+
break if not ids.include?(next_id)
|
145
|
+
count += 1
|
146
|
+
end
|
147
|
+
safely do
|
148
|
+
image = {
|
149
|
+
:name => opts[:name],
|
150
|
+
:owner_id => 'root',
|
151
|
+
:description => opts[:description],
|
152
|
+
:architecture => 'i386',
|
153
|
+
:state => 'UP'
|
154
|
+
}
|
155
|
+
File.open( "#{@storage_root}/images/#{next_id}.yml", 'w' ) do |f|
|
156
|
+
YAML.dump( image, f )
|
157
|
+
end
|
158
|
+
image[:id] = next_id
|
159
|
+
Image.new(image)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
131
163
|
#
|
132
164
|
# Instances
|
133
165
|
#
|
@@ -190,6 +222,7 @@ class MockDriver < Deltacloud::BaseDriver
|
|
190
222
|
:private_addresses=>["#{image_id}.#{next_id}.private.com"],
|
191
223
|
:instance_profile => InstanceProfile.new(hwp.name, opts),
|
192
224
|
:realm_id=>realm_id,
|
225
|
+
:create_image=>true,
|
193
226
|
:actions=>instance_actions_for( 'RUNNING' )
|
194
227
|
}
|
195
228
|
File.open( "#{@storage_root}/instances/#{next_id}.yml", 'w' ) {|f|
|
@@ -311,73 +344,145 @@ class MockDriver < Deltacloud::BaseDriver
|
|
311
344
|
#--
|
312
345
|
# Buckets
|
313
346
|
#--
|
314
|
-
def buckets(credentials, opts=
|
347
|
+
def buckets(credentials, opts={})
|
315
348
|
check_credentials(credentials)
|
316
349
|
buckets=[]
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
350
|
+
safely do
|
351
|
+
unless (opts[:id].nil?)
|
352
|
+
bucket_file = File::join(@storage_root, 'buckets', "#{opts[:id]}.yml")
|
353
|
+
bucket = YAML.load_file(bucket_file)
|
354
|
+
bucket[:id] = opts[:id]
|
355
|
+
bucket[:name] = bucket[:id]
|
356
|
+
buckets << Bucket.new( bucket )
|
357
|
+
else
|
358
|
+
Dir[ File::join(@storage_root, 'buckets', '*.yml')].each do |bucket_file|
|
359
|
+
bucket_id = File.basename( bucket_file, ".yml" )
|
360
|
+
buckets << Bucket.new( {:id => bucket_id, :name => bucket_id } )
|
361
|
+
end
|
362
|
+
end
|
322
363
|
end
|
323
364
|
buckets = filter_on( buckets, :id, opts )
|
324
|
-
buckets
|
325
365
|
end
|
326
366
|
|
327
367
|
#--
|
328
368
|
# Create bucket
|
329
369
|
#--
|
330
|
-
def create_bucket(credentials, name, opts=
|
370
|
+
def create_bucket(credentials, name, opts={})
|
331
371
|
check_credentials(credentials)
|
332
372
|
bucket = {
|
373
|
+
:id => name,
|
333
374
|
:name=>name,
|
334
375
|
:size=>'0',
|
335
376
|
:blob_list=>[]
|
336
377
|
}
|
337
|
-
File.open(
|
378
|
+
File.open( File::join(@storage_root, 'buckets', "#{name}.yml"), 'w') {|b| YAML.dump( bucket, b )}
|
338
379
|
Bucket.new(bucket)
|
339
380
|
end
|
340
381
|
|
341
382
|
#--
|
342
383
|
# Delete bucket
|
343
384
|
#--
|
344
|
-
def delete_bucket(credentials, name, opts=
|
385
|
+
def delete_bucket(credentials, name, opts={})
|
386
|
+
check_credentials(credentials)
|
345
387
|
bucket = bucket(credentials, {:id => name})
|
346
388
|
unless (bucket.size == "0")
|
347
389
|
raise Deltacloud::BackendError.new(403, self.class.to_s, "bucket-not-empty", "delete operation not valid for non-empty bucket")
|
348
390
|
end
|
349
391
|
safely do
|
350
|
-
File.delete(
|
392
|
+
File.delete(File::join(@storage_root, 'buckets', "#{name}.yml"))
|
351
393
|
end
|
352
394
|
end
|
353
395
|
|
354
396
|
#--
|
355
397
|
# Blobs
|
356
398
|
#--
|
357
|
-
def blobs(credentials, opts =
|
399
|
+
def blobs(credentials, opts = {})
|
358
400
|
check_credentials(credentials)
|
359
401
|
blobs=[]
|
360
|
-
|
361
|
-
|
362
|
-
blob
|
402
|
+
blobfile = File::join("#{@storage_root}", "buckets", "blobs", "#{opts[:id]}.yml")
|
403
|
+
safely do
|
404
|
+
blob = YAML.load_file(blobfile)
|
405
|
+
return [] unless blob[:bucket] == opts['bucket'] #can't return nil since base_driver invokes .first on return
|
406
|
+
blob[:id] = File.basename( blobfile, ".yml" )
|
363
407
|
blob[:name] = blob[:id]
|
364
408
|
blobs << Blob.new( blob )
|
409
|
+
blobs = filter_on( blobs, :id, opts )
|
365
410
|
end
|
366
|
-
blobs = filter_on( blobs, :id, opts )
|
367
|
-
blobs
|
368
411
|
end
|
369
412
|
|
370
413
|
#--
|
371
414
|
# Blob content
|
372
415
|
#--
|
373
|
-
def blob_data(credentials, bucket_id, blob_id, opts =
|
416
|
+
def blob_data(credentials, bucket_id, blob_id, opts = {})
|
374
417
|
check_credentials(credentials)
|
375
418
|
blob=nil
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
419
|
+
safely do
|
420
|
+
blobfile = File::join("#{@storage_root}", "buckets", "blobs", "#{opts['blob']}.yml")
|
421
|
+
blob = YAML.load_file(blobfile)
|
422
|
+
end
|
423
|
+
blob[:content].each {|part| yield part}
|
424
|
+
end
|
425
|
+
|
426
|
+
#--
|
427
|
+
# Create blob
|
428
|
+
#--
|
429
|
+
def create_blob(credentials, bucket_id, blob_id, blob_data, opts={})
|
430
|
+
check_credentials(credentials)
|
431
|
+
blob_meta = {}
|
432
|
+
opts.inject({}){|result, (k,v)| blob_meta[k] = v if k.match(/X[_-]Deltacloud[_-]Blobmeta[_-]/i)} #select{|k,v| k.match(/X[_-]Deltacloud[_-]Blobmeta[_-]/i)}
|
433
|
+
blob = {
|
434
|
+
:id => blob_id,
|
435
|
+
:bucket => bucket_id,
|
436
|
+
:content_length => blob_data[:tempfile].length,
|
437
|
+
:content_type => blob_data[:type],
|
438
|
+
:last_modified => Time.now,
|
439
|
+
:user_metadata => blob_meta.gsub_keys('X_Deltacloud_Blobmeta_', ''),
|
440
|
+
:content => blob_data[:tempfile].read
|
441
|
+
}
|
442
|
+
File.open( File::join("#{@storage_root}", "buckets", "blobs", "#{blob_id}.yml"), 'w' ) {|b| YAML.dump( blob, b )}
|
443
|
+
Blob.new(blob)
|
444
|
+
end
|
445
|
+
|
446
|
+
#--
|
447
|
+
# Delete blob
|
448
|
+
#--
|
449
|
+
def delete_blob(credentials, bucket_id, blob_id, opts={})
|
450
|
+
check_credentials(credentials)
|
451
|
+
blobfile = File::join("#{@storage_root}", "buckets", "blobs", "#{blob_id}.yml")
|
452
|
+
safely do
|
453
|
+
unless File.exists?(blobfile)
|
454
|
+
raise Deltacloud::BackendError.new(500, self.class.to_s, "blob #{blob_id} doesn't exist", "cannot delete non existant blob")
|
380
455
|
end
|
456
|
+
File.delete(blobfile)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
#--
|
461
|
+
# Get metadata
|
462
|
+
#--
|
463
|
+
def blob_metadata(credentials, opts={})
|
464
|
+
check_credentials(credentials)
|
465
|
+
blobfile = File::join("#{@storage_root}", "buckets", "blobs", "#{opts[:id]}.yml")
|
466
|
+
#safely do - mechanism not suitable here since head requests don't return a body response
|
467
|
+
begin
|
468
|
+
blob = YAML.load_file(blobfile)
|
469
|
+
rescue Errno::ENOENT
|
470
|
+
return nil #server.rb picks this up and gives 404
|
471
|
+
end
|
472
|
+
blob[:user_metadata]
|
473
|
+
end
|
474
|
+
|
475
|
+
#--
|
476
|
+
# Update metadata
|
477
|
+
#--
|
478
|
+
def update_blob_metadata(credentials, opts={})
|
479
|
+
check_credentials(credentials)
|
480
|
+
blobfile = File::join("#{@storage_root}", "buckets", "blobs", "#{opts[:id]}.yml")
|
481
|
+
safely do
|
482
|
+
blob = YAML.load_file(blobfile)
|
483
|
+
return false unless blob
|
484
|
+
blob[:user_metadata] = opts['meta_hash'].gsub_keys('HTTP[-_]X[-_]Deltacloud[-_]Blobmeta[-_]', '')
|
485
|
+
File.open(File::join("#{@storage_root}", "buckets", "blobs", "#{opts[:id]}.yml"), 'w' ) {|b| YAML.dump( blob, b )}
|
381
486
|
end
|
382
487
|
end
|
383
488
|
|
@@ -393,15 +498,18 @@ class MockDriver < Deltacloud::BaseDriver
|
|
393
498
|
private
|
394
499
|
|
395
500
|
def check_credentials(credentials)
|
396
|
-
if ( credentials.user != 'mockuser' )
|
397
|
-
raise Deltacloud::AuthException.new
|
398
|
-
end
|
399
|
-
|
400
|
-
if ( credentials.password != 'mockpassword' )
|
501
|
+
if ( credentials.user != 'mockuser' ) or ( credentials.password != 'mockpassword' )
|
401
502
|
raise Deltacloud::AuthException.new
|
402
503
|
end
|
403
504
|
end
|
404
505
|
|
506
|
+
def catched_exceptions_list
|
507
|
+
{
|
508
|
+
:auth => [],
|
509
|
+
:error => [ /Deltacloud::BackendError/, /Errno::ENOENT/ ],
|
510
|
+
:glob => [ /Error/ ]
|
511
|
+
}
|
512
|
+
end
|
405
513
|
|
406
514
|
end
|
407
515
|
|
@@ -1,21 +1,19 @@
|
|
1
|
-
#--------------------------------------------------------------------------- #
|
2
|
-
# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
|
3
|
-
# Complutense de Madrid (dsa-research.org)
|
4
1
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more
|
3
|
+
# contributor license agreements. See the NOTICE file distributed with
|
4
|
+
# this work for additional information regarding copyright ownership. The
|
5
|
+
# ASF licenses this file to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance with the
|
7
|
+
# License. You may obtain a copy of the License at
|
9
8
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
15
|
+
# under the License.
|
14
16
|
#
|
15
|
-
# You should have received a copy of the GNU Lesser General Public
|
16
|
-
# License along with this library; if not, write to the Free Software
|
17
|
-
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
|
-
#--------------------------------------------------------------------------- #
|
19
17
|
|
20
18
|
require 'rubygems'
|
21
19
|
require 'uri'
|
@@ -1,21 +1,19 @@
|
|
1
|
-
#--------------------------------------------------------------------------- #
|
2
|
-
# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
|
3
|
-
# Complutense de Madrid (dsa-research.org)
|
4
1
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more
|
3
|
+
# contributor license agreements. See the NOTICE file distributed with
|
4
|
+
# this work for additional information regarding copyright ownership. The
|
5
|
+
# ASF licenses this file to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance with the
|
7
|
+
# License. You may obtain a copy of the License at
|
9
8
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
15
|
+
# under the License.
|
14
16
|
#
|
15
|
-
# You should have received a copy of the GNU Lesser General Public
|
16
|
-
# License along with this library; if not, write to the Free Software
|
17
|
-
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
|
-
#--------------------------------------------------------------------------- #
|
19
17
|
|
20
18
|
require 'rubygems'
|
21
19
|
require 'uri'
|
@@ -1,21 +1,19 @@
|
|
1
|
-
#--------------------------------------------------------------------------- #
|
2
|
-
# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
|
3
|
-
# Complutense de Madrid (dsa-research.org)
|
4
1
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more
|
3
|
+
# contributor license agreements. See the NOTICE file distributed with
|
4
|
+
# this work for additional information regarding copyright ownership. The
|
5
|
+
# ASF licenses this file to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance with the
|
7
|
+
# License. You may obtain a copy of the License at
|
9
8
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
15
|
+
# under the License.
|
14
16
|
#
|
15
|
-
# You should have received a copy of the GNU Lesser General Public
|
16
|
-
# License along with this library; if not, write to the Free Software
|
17
|
-
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
|
-
#--------------------------------------------------------------------------- #
|
19
17
|
|
20
18
|
require 'deltacloud/base_driver'
|
21
19
|
|
@@ -1,6 +1,4 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (C) 2009 Red Hat, Inc.
|
3
|
-
#
|
4
2
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
5
3
|
# contributor license agreements. See the NOTICE file distributed with
|
6
4
|
# this work for additional information regarding copyright ownership. The
|
@@ -17,8 +15,9 @@
|
|
17
15
|
# under the License.
|
18
16
|
|
19
17
|
require 'deltacloud/base_driver'
|
20
|
-
require 'deltacloud/drivers/rackspace/rackspace_client'
|
21
18
|
require 'cloudfiles'
|
19
|
+
require 'cloudservers'
|
20
|
+
require 'base64'
|
22
21
|
|
23
22
|
module Deltacloud
|
24
23
|
module Drivers
|
@@ -27,20 +26,22 @@ module Deltacloud
|
|
27
26
|
class RackspaceDriver < Deltacloud::BaseDriver
|
28
27
|
|
29
28
|
feature :instances, :user_name
|
29
|
+
feature :instances, :authentication_password
|
30
|
+
feature :instances, :user_files
|
30
31
|
|
31
32
|
def supported_collections
|
32
|
-
DEFAULT_COLLECTIONS + [ :buckets ]
|
33
|
+
DEFAULT_COLLECTIONS + [ :buckets ] - [ :storage_snapshots, :storage_volumes ]
|
33
34
|
end
|
34
35
|
|
35
|
-
def hardware_profiles(credentials, opts =
|
36
|
-
|
37
|
-
results=
|
36
|
+
def hardware_profiles(credentials, opts = {})
|
37
|
+
rs = new_client( credentials )
|
38
|
+
results = []
|
38
39
|
safely do
|
39
|
-
results =
|
40
|
-
HardwareProfile.new(
|
40
|
+
results = rs.list_flavors.collect do |f|
|
41
|
+
HardwareProfile.new(f[:id].to_s) do
|
41
42
|
architecture 'x86_64'
|
42
|
-
memory
|
43
|
-
storage
|
43
|
+
memory f[:ram].to_i
|
44
|
+
storage f[:disk].to_i
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
@@ -48,22 +49,21 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def images(credentials, opts=nil)
|
51
|
-
|
52
|
-
results=
|
52
|
+
rs = new_client(credentials)
|
53
|
+
results = []
|
53
54
|
safely do
|
54
|
-
results =
|
55
|
-
Image.new(
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
55
|
+
results = rs.list_images.collect do |img|
|
56
|
+
Image.new(
|
57
|
+
:id => img[:id].to_s,
|
58
|
+
:name => img[:name],
|
59
|
+
:description => img[:name],
|
60
|
+
:owner_id => credentials.user,
|
61
|
+
:state => img[:status],
|
62
|
+
:architecture => 'x86_64'
|
63
|
+
)
|
62
64
|
end
|
63
65
|
end
|
64
|
-
results
|
65
|
-
results = filter_on( results, :id, opts )
|
66
|
-
results
|
66
|
+
filter_on( results, :id, opts )
|
67
67
|
end
|
68
68
|
|
69
69
|
#rackspace does not at this stage have realms... its all US/TX, all the time (at least at time of writing)
|
@@ -75,67 +75,99 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
75
75
|
} )]
|
76
76
|
end
|
77
77
|
|
78
|
-
|
79
|
-
|
78
|
+
#
|
79
|
+
# create instance. Default to flavor 1 - really need a name though...
|
80
|
+
# In rackspace, all flavors work with all images.
|
81
|
+
#
|
82
|
+
def create_instance(credentials, image_id, opts)
|
83
|
+
rs = new_client( credentials )
|
84
|
+
result = nil
|
85
|
+
params = extract_personality(opts)
|
86
|
+
params[:name] = opts[:name] || Time.now.to_s
|
87
|
+
params[:imageId] = image_id.to_i
|
88
|
+
params[:flavorId] = (opts[:hwp_id] && opts[:hwp_id].length>0) ? opts[:hwp_id].to_i : 1
|
80
89
|
safely do
|
81
|
-
|
90
|
+
server = rs.create_server(params)
|
91
|
+
result = convert_instance_after_create(server, credentials.user, server.adminPass)
|
82
92
|
end
|
83
|
-
|
84
|
-
:id => id,
|
85
|
-
:state => "RUNNING",
|
86
|
-
:actions => instance_actions_for( "RUNNING" ),
|
87
|
-
} )
|
93
|
+
result
|
88
94
|
end
|
89
95
|
|
90
|
-
def
|
91
|
-
|
96
|
+
def create_image(credentials, opts={})
|
97
|
+
rs = new_client(credentials)
|
98
|
+
safely do
|
99
|
+
server = rs.get_server(opts[:id].to_i)
|
100
|
+
image = server.create_image(opts[:name])
|
101
|
+
Image.new(
|
102
|
+
:id => image.id.to_s,
|
103
|
+
:name => image.name,
|
104
|
+
:description => image.name,
|
105
|
+
:owner_id => credentials.user,
|
106
|
+
:state => image.status,
|
107
|
+
:architecture => 'x86_64'
|
108
|
+
)
|
109
|
+
end
|
92
110
|
end
|
93
111
|
|
94
|
-
def
|
95
|
-
|
112
|
+
def run_on_instance(credentials, opts={})
|
113
|
+
target = instance(credentials, :id => opts[:id])
|
114
|
+
param = {}
|
115
|
+
param[:credentials] = {
|
116
|
+
:username => 'root',
|
117
|
+
:password => opts[:password]
|
118
|
+
}
|
119
|
+
param[:port] = opts[:port] || '22'
|
120
|
+
param[:ip] = target.public_addresses.first
|
96
121
|
safely do
|
97
|
-
|
122
|
+
Deltacloud::Runner.execute(opts[:cmd], param)
|
98
123
|
end
|
99
|
-
Instance.new( {
|
100
|
-
:id => id,
|
101
|
-
:state => "STOPPED",
|
102
|
-
:actions => instance_actions_for( "STOPPED" ),
|
103
|
-
} )
|
104
124
|
end
|
105
125
|
|
126
|
+
def reboot_instance(credentials, instance_id)
|
127
|
+
rs = new_client(credentials)
|
128
|
+
safely do
|
129
|
+
server = rs.get_server(instance_id.to_i)
|
130
|
+
server.reboot!
|
131
|
+
convert_instance_after_create(server, credentials.user)
|
132
|
+
end
|
133
|
+
end
|
106
134
|
|
107
|
-
|
108
|
-
|
109
|
-
# In rackspace, all flavors work with all images.
|
110
|
-
#
|
111
|
-
def create_instance(credentials, image_id, opts)
|
112
|
-
racks = new_client( credentials )
|
113
|
-
hwp_id = opts[:hwp_id] || 1
|
114
|
-
name = Time.now.to_s
|
115
|
-
if (opts[:name]) then name = opts[:name] end
|
135
|
+
def destroy_instance(credentials, instance_id)
|
136
|
+
rs = new_client(credentials)
|
116
137
|
safely do
|
117
|
-
|
138
|
+
server = rs.get_server(instance_id.to_i)
|
139
|
+
server.delete!
|
140
|
+
convert_instance_after_create(server, credentials.user)
|
118
141
|
end
|
119
142
|
end
|
120
143
|
|
144
|
+
alias_method :stop_instance, :destroy_instance
|
145
|
+
|
121
146
|
#
|
122
147
|
# Instances
|
123
148
|
#
|
124
|
-
def instances(credentials, opts=
|
125
|
-
|
126
|
-
|
149
|
+
def instances(credentials, opts={})
|
150
|
+
|
151
|
+
rs = new_client(credentials)
|
152
|
+
insts = []
|
153
|
+
|
127
154
|
safely do
|
128
|
-
|
129
|
-
|
130
|
-
|
155
|
+
begin
|
156
|
+
if opts[:id]
|
157
|
+
server = rs.get_server(opts[:id].to_i)
|
158
|
+
insts << convert_instance_after_create(server, credentials.user)
|
159
|
+
else
|
160
|
+
insts = rs.list_servers_detail.collect do |server|
|
161
|
+
convert_instance(server, credentials.user)
|
162
|
+
end
|
131
163
|
end
|
132
|
-
|
133
|
-
instances << convert_srv_to_instance(racks.load_server_details(opts[:id]))
|
164
|
+
rescue CloudServers::Exception::ItemNotFound
|
134
165
|
end
|
135
166
|
end
|
136
|
-
|
137
|
-
|
138
|
-
|
167
|
+
|
168
|
+
insts = filter_on( insts, :id, opts )
|
169
|
+
insts = filter_on( insts, :state, opts )
|
170
|
+
insts
|
139
171
|
end
|
140
172
|
|
141
173
|
def valid_credentials?(credentials)
|
@@ -147,42 +179,38 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
147
179
|
true
|
148
180
|
end
|
149
181
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
182
|
define_instance_states do
|
154
183
|
start.to( :pending ) .on( :create )
|
155
|
-
|
156
184
|
pending.to( :running ) .automatically
|
157
|
-
|
158
185
|
running.to( :running ) .on( :reboot )
|
159
186
|
running.to( :shutting_down ) .on( :stop )
|
160
|
-
|
161
187
|
shutting_down.to( :stopped ) .automatically
|
162
|
-
|
163
188
|
stopped.to( :finish ) .automatically
|
164
189
|
end
|
165
190
|
|
166
191
|
#--
|
167
192
|
# Buckets
|
168
193
|
#--
|
169
|
-
def buckets(credentials, opts)
|
194
|
+
def buckets(credentials, opts = {})
|
170
195
|
bucket_list = []
|
171
196
|
cf = cloudfiles_client(credentials)
|
172
197
|
safely do
|
173
|
-
|
174
|
-
|
175
|
-
bucket_list << convert_container(
|
176
|
-
|
198
|
+
unless (opts[:id].nil?)
|
199
|
+
bucket = cf.container(opts[:id])
|
200
|
+
bucket_list << convert_container(bucket)
|
201
|
+
else
|
202
|
+
cf.containers.each do |container_name|
|
203
|
+
bucket_list << Bucket.new({:id => container_name, :name => container_name})
|
204
|
+
end #containers.each
|
205
|
+
end #unless
|
177
206
|
end #safely
|
178
|
-
|
179
|
-
bucket_list
|
207
|
+
filter_on(bucket_list, :id, opts)
|
180
208
|
end
|
181
209
|
|
182
210
|
#--
|
183
211
|
# Create Bucket
|
184
212
|
#--
|
185
|
-
def create_bucket(credentials, name, opts)
|
213
|
+
def create_bucket(credentials, name, opts = {})
|
186
214
|
bucket = nil
|
187
215
|
cf = cloudfiles_client(credentials)
|
188
216
|
safely do
|
@@ -195,7 +223,7 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
195
223
|
#--
|
196
224
|
# Delete Bucket
|
197
225
|
#--
|
198
|
-
def delete_bucket(credentials, name, opts)
|
226
|
+
def delete_bucket(credentials, name, opts = {})
|
199
227
|
cf = cloudfiles_client(credentials)
|
200
228
|
safely do
|
201
229
|
cf.delete_container(name)
|
@@ -205,7 +233,7 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
205
233
|
#--
|
206
234
|
# Blobs
|
207
235
|
#--
|
208
|
-
def blobs(credentials, opts)
|
236
|
+
def blobs(credentials, opts = {})
|
209
237
|
cf = cloudfiles_client(credentials)
|
210
238
|
blobs = []
|
211
239
|
safely do
|
@@ -221,7 +249,7 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
221
249
|
#-
|
222
250
|
# Blob data
|
223
251
|
#-
|
224
|
-
def blob_data(credentials, bucket_id, blob_id, opts)
|
252
|
+
def blob_data(credentials, bucket_id, blob_id, opts = {})
|
225
253
|
cf = cloudfiles_client(credentials)
|
226
254
|
cf.container(bucket_id).object(blob_id).data_stream do |chunk|
|
227
255
|
yield chunk
|
@@ -231,18 +259,25 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
231
259
|
#--
|
232
260
|
# Create Blob
|
233
261
|
#--
|
234
|
-
def create_blob(credentials, bucket_id, blob_id, blob_data, opts=
|
262
|
+
def create_blob(credentials, bucket_id, blob_id, blob_data, opts={})
|
235
263
|
cf = cloudfiles_client(credentials)
|
236
|
-
#
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
264
|
+
#insert ec2-specific header for user metadata ... X-Object-Meta-KEY = VALUE
|
265
|
+
opts.gsub_keys("HTTP_X_Deltacloud_Blobmeta_", "X-Object-Meta-")
|
266
|
+
opts['Content-Type'] = blob_data[:type]
|
267
|
+
object = nil
|
268
|
+
safely do
|
269
|
+
#must first create the object using cloudfiles_client.create_object
|
270
|
+
#then can write using object.write(data)
|
271
|
+
object = cf.container(bucket_id).create_object(blob_id)
|
272
|
+
#blob_data is a construct with data in .tempfile and content-type in {:type}
|
273
|
+
res = object.write(blob_data[:tempfile], opts)
|
274
|
+
end
|
241
275
|
Blob.new( { :id => object.name,
|
242
276
|
:bucket => object.container.name,
|
243
277
|
:content_length => blob_data[:tempfile].length,
|
244
278
|
:content_type => blob_data[:type],
|
245
|
-
:last_modified => ''
|
279
|
+
:last_modified => '',
|
280
|
+
:user_metadata => opts.select{|k,v| k.match(/^X-Object-Meta-/i)}
|
246
281
|
}
|
247
282
|
)
|
248
283
|
end
|
@@ -250,32 +285,45 @@ class RackspaceDriver < Deltacloud::BaseDriver
|
|
250
285
|
#--
|
251
286
|
# Delete Blob
|
252
287
|
#--
|
253
|
-
def delete_blob(credentials, bucket_id, blob_id, opts=
|
288
|
+
def delete_blob(credentials, bucket_id, blob_id, opts={})
|
289
|
+
cf = cloudfiles_client(credentials)
|
290
|
+
safely do
|
291
|
+
cf.container(bucket_id).delete_object(blob_id)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
#-
|
296
|
+
# Blob Metadada
|
297
|
+
#-
|
298
|
+
def blob_metadata(credentials, opts = {})
|
299
|
+
cf = cloudfiles_client(credentials)
|
300
|
+
meta = {}
|
301
|
+
safely do
|
302
|
+
meta = cf.container(opts['bucket']).object(opts[:id]).metadata
|
303
|
+
end
|
304
|
+
meta
|
305
|
+
end
|
306
|
+
|
307
|
+
#-
|
308
|
+
# Update Blob Metahash
|
309
|
+
#-
|
310
|
+
def update_blob_metadata(credentials, opts={})
|
254
311
|
cf = cloudfiles_client(credentials)
|
255
|
-
|
312
|
+
meta_hash = opts['meta_hash']
|
313
|
+
#the set_metadata method actually places the 'X-Object-Meta-' prefix for us:
|
314
|
+
meta_hash.gsub_keys('HTTP_X_Deltacloud_Blobmeta_', '')
|
315
|
+
safely do
|
316
|
+
blob = cf.container(opts['bucket']).object(opts[:id])
|
317
|
+
blob.set_metadata(meta_hash)
|
318
|
+
end
|
256
319
|
end
|
257
320
|
|
258
321
|
private
|
259
322
|
|
260
|
-
def convert_srv_to_instance(srv)
|
261
|
-
inst = Instance.new(:id => srv["id"].to_s,
|
262
|
-
:owner_id => "root",
|
263
|
-
:realm_id => "us")
|
264
|
-
inst.name = srv["name"]
|
265
|
-
inst.state = srv["status"] == "ACTIVE" ? "RUNNING" : "PENDING"
|
266
|
-
inst.actions = instance_actions_for(inst.state)
|
267
|
-
inst.image_id = srv["imageId"].to_s
|
268
|
-
inst.instance_profile = InstanceProfile.new(srv["flavorId"].to_s)
|
269
|
-
if srv["addresses"]
|
270
|
-
inst.public_addresses = srv["addresses"]["public"]
|
271
|
-
inst.private_addresses = srv["addresses"]["private"]
|
272
|
-
end
|
273
|
-
inst
|
274
|
-
end
|
275
323
|
|
276
324
|
def new_client(credentials)
|
277
325
|
safely do
|
278
|
-
|
326
|
+
CloudServers::Connection.new(:username => credentials.user, :api_key => credentials.password)
|
279
327
|
end
|
280
328
|
end
|
281
329
|
|
@@ -292,24 +340,87 @@ private
|
|
292
340
|
:bucket => cf_object.container.name,
|
293
341
|
:content_length => cf_object.bytes,
|
294
342
|
:content_type => cf_object.content_type,
|
295
|
-
:last_modified => cf_object.last_modified
|
343
|
+
:last_modified => cf_object.last_modified,
|
344
|
+
:user_metadata => cf_object.metadata
|
296
345
|
})
|
297
346
|
end
|
298
347
|
|
348
|
+
def convert_instance_after_create(server, user_name, password='')
|
349
|
+
inst = Instance.new(
|
350
|
+
:id => server.id.to_s,
|
351
|
+
:realm_id => 'us',
|
352
|
+
:owner_id => user_name,
|
353
|
+
:description => server.name,
|
354
|
+
:name => server.name,
|
355
|
+
:state => (server.status == 'ACTIVE') ? 'RUNNING' : 'PENDING',
|
356
|
+
:architecture => 'x86_64',
|
357
|
+
:image_id => server.imageId.to_s,
|
358
|
+
:instance_profile => InstanceProfile::new(server.flavorId.to_s),
|
359
|
+
:public_addresses => server.addresses[:public],
|
360
|
+
:private_addresses => server.addresses[:private],
|
361
|
+
:username => 'root',
|
362
|
+
:password => password ? password : nil
|
363
|
+
)
|
364
|
+
inst.actions = instance_actions_for(inst.state)
|
365
|
+
inst.create_image = 'RUNNING'.eql?(inst.state)
|
366
|
+
inst
|
367
|
+
end
|
368
|
+
|
369
|
+
def convert_instance(server, user_name = '')
|
370
|
+
inst = Instance.new(
|
371
|
+
:id => server[:id].to_s,
|
372
|
+
:realm_id => 'us',
|
373
|
+
:owner_id => user_name,
|
374
|
+
:description => server[:name],
|
375
|
+
:name => server[:name],
|
376
|
+
:state => (server[:status] == 'ACTIVE') ? 'RUNNING' : 'PENDING',
|
377
|
+
:architecture => 'x86_64',
|
378
|
+
:image_id => server[:imageId].to_s,
|
379
|
+
:instance_profile => InstanceProfile::new(server[:flavorId].to_s),
|
380
|
+
:public_addresses => server[:addresses][:public],
|
381
|
+
:private_addresses => server[:addresses][:private]
|
382
|
+
)
|
383
|
+
inst.create_image = 'RUNNING'.eql?(inst.state)
|
384
|
+
inst.actions = instance_actions_for(inst.state)
|
385
|
+
inst
|
386
|
+
end
|
387
|
+
|
299
388
|
def cloudfiles_client(credentials)
|
300
389
|
safely do
|
301
390
|
CloudFiles::Connection.new(:username => credentials.user, :api_key => credentials.password)
|
302
391
|
end
|
303
392
|
end
|
304
393
|
|
305
|
-
def
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
394
|
+
def catched_exceptions_list
|
395
|
+
{
|
396
|
+
:auth => [ /Authentication failed/ ],
|
397
|
+
:error => [ /Error/ ],
|
398
|
+
:glob => [ /CloudServers::Exception::(\w+)/, /Deltacloud::Runner::(\w+)/ ]
|
399
|
+
}
|
311
400
|
end
|
312
401
|
|
402
|
+
private
|
403
|
+
|
404
|
+
def extract_personality(opts)
|
405
|
+
# This relies on an undocumented feature of the cloudservers gem:
|
406
|
+
# create_server allows passing in strings for the file contents
|
407
|
+
# directly if :personality maps to an array of hashes
|
408
|
+
ary = opts.inject([]) do |a, e|
|
409
|
+
k, v = e
|
410
|
+
if k.to_s =~ /^path([0-9]+)/
|
411
|
+
a << {
|
412
|
+
:path => v,
|
413
|
+
:contents => Base64.decode64(opts[:"content#{$1}"])
|
414
|
+
}
|
415
|
+
end
|
416
|
+
a
|
417
|
+
end
|
418
|
+
if ary.empty?
|
419
|
+
{}
|
420
|
+
else
|
421
|
+
{ :personality => ary }
|
422
|
+
end
|
423
|
+
end
|
313
424
|
end
|
314
425
|
|
315
426
|
end
|