cli cloud stuff

add, remove and list cloud cli commands complete as well as db models
tweaked to high hell..
This commit is contained in:
David Lenwell 2013-10-03 22:23:10 -07:00
parent 0379f00772
commit f90cac1452
14 changed files with 719 additions and 123 deletions

View File

@ -42,7 +42,7 @@ fileConfig(config.config_file_name)
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None
from refstack.web import db
from refstack.models import db
target_metadata = db.metadata

View File

@ -0,0 +1,25 @@
"""Adding Cloud and User models
Revision ID: 23f82f78731b
Revises: 40d4c6d389ec
Create Date: 2013-07-02 15:02:46.951119
"""
# revision identifiers, used by Alembic.
revision = '23f82f78731b'
down_revision = '40d4c6d389ec'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
pass
def downgrade():
### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###

View File

@ -0,0 +1,22 @@
"""remove unique restraints from cloud add one to users
Revision ID: 2bf99fa45ded
Revises: 23f82f78731b
Create Date: 2013-09-20 06:14:43.246339
"""
# revision identifiers, used by Alembic.
revision = '2bf99fa45ded'
down_revision = '23f82f78731b'
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass

View File

@ -30,4 +30,4 @@ def upgrade():
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('vendor')
### end Alembic commands ###
### end Alembic commands ###

View File

@ -35,12 +35,7 @@ def upgrade():
sa.Column('admin_key', sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(['vendor_id'], ['vendor.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('admin_endpoint'),
sa.UniqueConstraint('admin_key'),
sa.UniqueConstraint('admin_user'),
sa.UniqueConstraint('endpoint'),
sa.UniqueConstraint('test_key'),
sa.UniqueConstraint('test_user')
sa.UniqueConstraint('endpoint')
)
### end Alembic commands ###

View File

@ -17,6 +17,8 @@
import os
from flask import Flask, session
db_path = os.path.abspath(
os.path.join(os.path.basename(__file__), "../"))

View File

@ -14,92 +14,187 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os.path
import sys
import subprocess
"""
"""
import argparse
from textwrap import dedent
from refstack.common.test import Test
basedir = os.path.dirname(__file__)
def call(*args, **kwargs):
"""subprocess.call with error checking."""
assert not subprocess.call(*args, **kwargs)
class actions(argparse.Action):
""" david please comment this """
test_id = None
params = {}
def __call__(self, parser, params, values, option_string=None, **kwargs):
"""Triggered by -c command line argument """
self.params = params
setattr(self.params, self.dest, values)
self._print(self.params, verbose=True)
# action function mapping
actions = { 'run' : self.run,
'status' : self.status,
'cancel' : self.cancel,
'result' : self.result,
'export' : self.export,
'list' : self.list }
if len(self.params.command) > 1:
print "Please only use one command at a time!\n\n"
parser.print_help()
sys.exit(1)
for command in self.params.command:
if command in actions:
actions[command]()
from sqlalchemy.exc import IntegrityError
from refstack.localmodels import *
def _print(self, output, verbose=False):
""" print wrapper so -v and -s can be respected """
if not self.params.silent:
if verbose is False:
print(output)
elif verbose is True and self.params.verbose > 0:
print(output)
def add(args):
"""add cloud
refstack add --endpoint='http://127.0.0.1:5000/v3/' --test-user='demo'
--test-key='pass' --admin-endpoint='http://127.0.0.1:5000/v3/'
--admin-user='admin' --admin-key='pass'
outputs confirmation along with the id of the cloud that was just added.
endpoint is a unique key so you only get to add one record per end point"""
try:
cloud = Cloud(args.endpoint,
args.test_user,
args.test_key,
args.admin_endpoint,
args.admin_user,
args.admin_key)
db.add(cloud)
db.commit()
print 'New cloud added with id: %s ' % (cloud.id)
except IntegrityError:
print 'A Cloud with %s as its endpoint has already been added. ' % args.endpoint
def run(self):
""" run test command"""
self._print('run command called',True)
tester = Test()
self._print(tester.config)
def remove(args):
"""remove cloud
refstack remove {cloud_id}
confirms that cloud-id 123 has been removed from the database as well as
all tests assosiateed with it."""
cloud = db.query(Cloud).get(args.cloud_id)
if cloud is None:
print 'No cloud with id: %s ' % args.cloud_id
else:
db.delete(cloud)
db.commit()
print 'cloud %s has been deleted.' % args.cloud_id
def status(self):
""" get the status of a running test"""
self._print('status command called',True)
def clouds(args):
"""returns either a list of cached tests"""
print 'Your clouds:\n'
print 'id | endpoint | test-user | admin-user '
print '---------------------------------------'
for row in db.query(Cloud).all():
print "%s | %s | %s | %s " % (row.id, row.endpoint, row.test_user, row.admin_user)
print ''
def cancel(self):
""" cancels a running test"""
self._print('cancel command called',True)
def run(args):
"""run test command
refstack run --cloud_id {123} --sha {sha}
triggers local run of tempest with specified cloud_id returns a
test_id so that the user can check status or cancel the test"""
print 'run triggered'
def result(self):
""" outputs the results of a test"""
self._print('result command called',True)
def status(args):
"""get the status of a running test
refstack status --test-id {123}
"""
print 'status triggered'
def export(self):
""" export something forgot why I adddedd this"""
self._print('export command called',True)
def cancel(args):
"""cancels a running test
refstack cancel --test-id {test_id}
stops the running test if it is running and displays output to user"""
print 'cancel triggered'
def list(self):
""" returns a list of cached test results"""
self._print('list command called',True)
def result(args):
"""outputs the results of a test
refstack results --test_id --format {screen|subunit}
if the test isn't finished it will say in progress otherwise will return
subunit|screen output"""
print 'result triggered'
def tests(args):
"""returns either a list of clouds"""
print 'tests triggered'
def clouds(args):
"""returns either a list of cached tests"""
print 'Your clouds:\n'
print 'id | endpoint | test-user | admin-user '
print '---------------------------------------'
for row in db.query(Cloud).all():
print "%s | %s | %s | %s " % (row.id, row.endpoint, row.test_user, row.admin_user)
print ''
def subcommands(subparsers):
"""argparse subparsers with """
add_cloud_parser = subparsers.add_parser('add', help='Add a new Cloud')
add_cloud_parser.add_argument('--endpoint',
required=True,
action='store',
dest='endpoint',
help='Non-admin keystone endpoint')
add_cloud_parser.add_argument('--test-user',
required=True,
action='store',
dest='test_user',
help='Non-admin keystone user')
add_cloud_parser.add_argument('--test-key',
required=True,
action='store',
dest='test_key',
help='Non-admin keystone password or key')
add_cloud_parser.add_argument('--admin-endpoint',
required=True,
action='store',
dest='admin_endpoint',
help='Admin keystone endpoint')
add_cloud_parser.add_argument('--admin-user',
required=True,
action='store',
dest='admin_user',
help='Admin keystone user')
add_cloud_parser.add_argument('--admin-key',
required=True,
action='store',
dest='admin_key',
help='Admin keystone key or password')
"""argparse options for the remove command """
remove_parser = subparsers.add_parser('remove', help='remove a Cloud')
remove_parser.add_argument(action='store',
dest='cloud_id',
help='The id of the cloud you want to remove')
"""argparse options for the run command """
run_parser = subparsers.add_parser('run', help='run tests on cloud')
"""argparse options for the status command """
status_parser = subparsers.add_parser('status', help='status of test')
"""argparse options for the cancel command """
cancel_parser = subparsers.add_parser('cancel', help='cancel a test')
"""argparse options for the result command """
result_parser = subparsers.add_parser('result', help='provides results')
"""argparse options for the tests command """
tests_parser = subparsers.add_parser('tests', help='list tests')
"""argparse options for the clouds command """
clouds_parser = subparsers.add_parser('clouds', help='list clouds')
def main():
""" command line hook """
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=dedent("""\
@ -112,32 +207,33 @@ def main():
Refstack CLI:
\n\n\n """))
# output options
parser.add_argument('--verbose', '-v', action='count')
parser.add_argument('--silent', '-s', action='store_true')
# all paramaters can be overridden
parser.add_argument('--test-id', nargs='?', type=int,
default=None,
help="""id of the test you want to interact with.. if you are starting
a new test you can leave this blank and it will
return a new test_id""")
subparsers = parser.add_subparsers(help='Sub commands', dest='command')
parser.add_argument('command', nargs='+', action=actions,
help="Command to execute. ",
choices=['run','status','cancel','result','export','list'] )
subcommands(subparsers)
args = parser.parse_args()
# validate input
#option_given = not (args.c == None)
#if not option_given:
# parser.print_help()
# sys.exit(1)
# action function mapping
actions = { 'add': add,
'remove': remove,
'run': run,
'status': status,
'cancel': cancel,
'result': result,
'tests': tests,
'clouds': clouds}
if args.command in actions:
actions[args.command](args)
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()
main()

222
refstack/cli/refstack_old Executable file
View File

@ -0,0 +1,222 @@
#!/usr/bin/env python
#
# Copyright (c) 2013 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os.path
import sys
import subprocess
import argparse
from textwrap import dedent
from refstack.common.test import Test
from refstack.common.cloud import Cloud
basedir = os.path.dirname(__file__)
def call(*args, **kwargs):
"""subprocess.call with error checking."""
assert not subprocess.call(*args, **kwargs)
class actions(argparse.Action):
""" david please comment this """
test_id = None
params = {}
def __call__(self, parser, params, values, option_string=None, **kwargs):
"""Triggered by -c command line argument """
self.params = params
setattr(self.params, self.dest, values)
self._print(self.params, verbose=False)
# action function mapping
actions = { 'add' : self.add,
'remove' : self.remove,
'run' : self.run,
'status' : self.status,
'cancel' : self.cancel,
'result' : self.result,
'list-tests' : self.list_tests,
'list-clouds' : self.list_clouds }
if len(self.params.command) > 1:
print "Please only use one command at a time!\n\n"
parser.print_help()
sys.exit(1)
for command in self.params.command:
if command in actions:
actions[command]()
def _print(self, output, verbose=False):
"""print wrapper so -v and -s can be respected"""
if not self.params.silent:
if verbose is False:
print(output)
elif verbose is True and self.params.verbose > 0:
print(output)
def add(self):
""" add new cloud
needs endpoint,test_user,test_key, admin_endpoint,admin_user,admin_key
refstack add --endpoint='http://127.0.0.1:5000/v3/' --test_user='demo'
--test_key='pass' --admin_endpoint='http://127.0.0.1:5000/v3/'
--admin_user='admin' --admin_key='pass'
outputs confirmation along with the cloud_id that was just created."""
self._print('add command called')
#self._print(self.params.endpoint)
#for k,v in self.params.:
# self._print(k + ': ' + v)
c = Cloud()
c.add(self.params.endpoint,self.params.test_user,self.params.test_key,
self.params.admin_endpoint,self.params.admin_user,self.params.admin_key)
self._print('cloud added ')
def remove(self):
""" add new cloud
refstack remove --cloud_id 123
confirms that cloud-id 123 has been removed from the database as well as
all tests assosiateed with it."""
self._print('remove command called',True)
def run(self):
""" run test command
refstack run --cloud_id {123} --sha {sha}
triggers local run of tempest with specified cloud_id returns a
test_id so that the user can check status or cancel the test"""
self._print('run command called',True)
def status(self):
""" get the status of a running test
refstack status --test-id {123}
"""
self._print('status command called',True)
def cancel(self):
""" cancels a running test
refstack cancel --test-id {test_id}
stops the running test if it is running and displays output to user"""
self._print('cancel command called',True)
def result(self):
""" outputs the results of a test
refstack results --test_id --format {screen|subunit}
if the test isn't finished it will say in progress otherwise will return
subunit|screen output"""
self._print('result command called',True)
def list_tests(self):
""" returns either a list of cached test results or a list of clouds
"""
self._print('list command called',True)
def list_clouds(self):
""" returns either a list of cached test results or a list of clouds
"""
self._print('list command called',True)
def main():
""" command line hook """
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=dedent("""\
This is a CLI utility for refstack
"""),
epilog=dedent("""\
Usage:
Refstack CLI:
\n\n\n """))
# output options
parser.add_argument('--verbose', '-v', action='count')
parser.add_argument('--silent', '-s', action='store_true')
# params for add endpoint,test_user,test_key, admin_endpoint,admin_user,admin_key
parser.add_argument('--endpoint', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--test_user', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--test_key', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--admin_endpoint', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--admin_user', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--admin_key', nargs='?', type=str,
default=None,
help="""""")
parser.add_argument('--test-id', nargs='?', type=int,
default=None,
help="""id of the test you want to interact with.. if you are starting
a new test you can leave this blank and it will
return a new test_id""")
parser.add_argument('command', nargs='+', action=actions,
help="Command to execute. ",
choices=[ 'run','add','remove',
'status','cancel','result',
'list_clouds','list_tests'])
args = parser.parse_args()
# validate input
#option_given = not (args.c == None)
#if not option_given:
# parser.print_help()
# sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -13,32 +13,55 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystoneclient.v2_0 import client
from refstack.common import *
import refstack.models
class Cloud(object):
""" Cloud functions"""
cloud_id = None
class Cloud:
""" Vendor functions"""
id = None
def __init__(self, id):
def __init__(self, cloud_id = None):
""" init method loads specified id or fails"""
self.id = id
self.cloud_id = cloud_id
@def end_point():
doc = "The end_point property."
def fget(self):
return self._end_point
def fset(self, value):
self._end_point = value
def fdel(self):
del self._end_point
return locals()
if not cloud_id:
# we have a new cloud.
# do nothing because now we'll call the add method
return None
else:
# load an existing cloud
self._cloud = models.Cloud.query.filter_by(id=self.cloud_id).first()
foo = property(**foo())
if not self._cloud:
# cloud not found.. invalid id
# maybe I should do someting about this ..
return None
def tests(self, cloud_
id = None):
""" returns object populated with test objects that belong to
this user and filter
ed by cloud_id if specified """
self._keystone = client.Client(username=self._cloud.admin_user,
password=self._cloud.admin_key,
auth_url=self._cloud.admin_endpoint )
self._end_point = None
def add(self,endpoint,test_user,test_key,
admin_endpoint,admin_user,admin_key,vendor_id=None):
#adds a new cloud to the db
models.db.session.add(Cloud(endpoint,test_user,test_key,
admin_endpoint,admin_user,admin_key,vendor_id))
models.db.session.commit()
@property
def end_point(self):
"""end_point property"""
return self._end_point
@end_point.setter
def end_point(self, value):
self._end_point = value
def get_config(self):
"""uses end_point and creditials from the specified cloud_id to
get a list of services and enpoints from keystone then outputs a
usable tempest config"""

View File

@ -13,11 +13,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from refstack.common.cloud import Cloud
class TempestConfig(object):
"""temptest config options. gets converted to a tempest config file"""
config = {}
def output(self):
"""outputs config in propper format"""
output = ''

53
refstack/localmodels.py Executable file
View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2013 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker,relationship, backref
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
engine = create_engine('sqlite:////tmp/refstack.db', convert_unicode=True)
db = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db.query_property()
class Cloud(Base):
__tablename__ = 'cloud'
id = Column(Integer, primary_key=True)
endpoint = Column(String(120), unique=True)
test_user = Column(String(80), unique=False)
test_key = Column(String(80), unique=False)
admin_endpoint = Column(String(120), unique=False)
admin_user = Column(String(80), unique=False)
admin_key = Column(String(80), unique=False)
def __init__(self,
endpoint,
test_user,
test_key,
admin_endpoint,
admin_user,
admin_key):
self.endpoint = endpoint
self.test_user = test_user
self.test_key = test_key
self.admin_endpoint = admin_endpoint
self.admin_user = admin_user
self.admin_key = admin_key

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
#
# Copyright (c) 2013 Piston Cloud Computing, Inc.
# All Rights Reserved.
@ -36,12 +35,12 @@ class Cloud(db.Model):
vendor = db.relationship('Vendor',
backref=db.backref('clouds',
lazy='dynamic'))
endpoint = db.Column(db.String(120), unique=True)
test_user = db.Column(db.String(80), unique=True)
test_key = db.Column(db.String(80), unique=True)
admin_endpoint = db.Column(db.String(120), unique=True)
admin_user = db.Column(db.String(80), unique=True)
admin_key = db.Column(db.String(80), unique=True)
endpoint = db.Column(db.String(120), unique=False)
test_user = db.Column(db.String(80), unique=False)
test_key = db.Column(db.String(80), unique=False)
admin_endpoint = db.Column(db.String(120), unique=False)
admin_user = db.Column(db.String(80), unique=False)
admin_key = db.Column(db.String(80), unique=False)
def __str__(self):
return self.endpoint
@ -50,7 +49,7 @@ class Cloud(db.Model):
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60))
email = db.Column(db.String(200))
email = db.Column(db.String(200), unique=True)
openid = db.Column(db.String(200), unique=True)
def __init__(self, name, email, openid):
@ -59,4 +58,4 @@ class User(db.Model):
self.openid = openid
def __str__(self):
return self.name
return self.name

156
refstack/templates/tempest.conf Executable file
View File

@ -0,0 +1,156 @@
[DEFAULT]
debug = True
#log_config = ${log_config}
use_stderr = False
log_file = ${log_file}
lock_path=${lock_path}
default_log_levels=tempest.stress=INFO,amqplib=WARN,sqlalchemy=WARN,boto=WARN,suds=INFO,keystone=INFO,eventlet.wsgi.server=WARN
[identity]
catalog_type = identity
disable_ssl_certificate_validation = False
uri = ${uri}
uri_v3 = ${uri_v3}
region = RegionOne
username = ${username}
password = ${pass}
tenant_name = ${tenant_name}
alt_username = alt_demo
alt_password = pass
alt_tenant_name = alt_demo
admin_username = admin
admin_password = pass
admin_tenant_name = admin
admin_role = admin
[compute]
allow_tenant_isolation = True
allow_tenant_reuse = true
image_ref = ${image_ref}
image_ref_alt = ${image_ref_alt}
flavor_ref = ${flavor_ref}
flavor_ref_alt = ${flavor_ref_alt}
image_ssh_user = ${image_ssh_user}
image_ssh_password = ${image_ssh_password}
image_alt_ssh_user = ${image_alt_ssh_user}
image_alt_ssh_password = ${image_alt_ssh_password}
build_interval = 1
build_timeout = 400
run_ssh = false
ssh_user = ${ssh_user}
fixed_network_name = ${fixed_network_name}
network_for_ssh = ${fixed_network_name}
ip_version_for_ssh = 4
ping_timeout = 60
ssh_timeout = 400
ready_wait = 0
ssh_channel_timeout = 60
use_floatingip_for_ssh = True
catalog_type = compute
#region = RegionOne
create_image_enabled = true
resize_available = true
change_password_available=False
live_migration_available = ${live_migration_availablea}
use_block_migration_for_live_migration = False
block_migrate_supports_cinder_iscsi = false
disk_config_enabled = true
flavor_extra_enabled = true
volume_device_name = ${volume_device_name}
[compute-admin]
username =
password =
tenant_name =
[image]
catalog_type = image
#region = RegionOne
api_version = 1
http_image = http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
[network]
api_version = 2.0
catalog_type = network
#region = RegionOne
tenant_network_cidr = 10.100.0.0/16
tenant_network_mask_bits = 28
tenant_networks_reachable = false
public_network_id =
public_router_id =
[volume]
catalog_type = volume
disk_format = raw
build_interval = 1
build_timeout = 400
multi_backend_enabled = false
backend1_name = BACKEND_1
backend2_name = BACKEND_2
storage_protocol = iSCSI
vendor_name = Open Source
[object-storage]
catalog_type = object-store
#region = RegionOne
container_sync_timeout = 120
container_sync_interval = 5
accounts_quotas_available = True
operator_role = Member
[boto]
ssh_user = cirros
ec2_url = http://172.16.200.130:8773/services/Cloud
s3_url = http://172.16.200.130:3333
aws_access =
aws_secret =
s3_materials_path = /home/stack/devstack/files/images/s3-materials/cirros-0.3.1
ari_manifest = cirros-0.3.1-x86_64-initrd.manifest.xml
ami_manifest = cirros-0.3.1-x86_64-blank.img.manifest.xml
aki_manifest = cirros-0.3.1-x86_64-vmlinuz.manifest.xml
instance_type = m1.nano
http_socket_timeout = 30
num_retries = 1
build_timeout = 400
build_interval = 1
[orchestration]
catalog_type = orchestration
#region = RegionOne
build_interval = 1
build_timeout = 300
instance_type = m1.micro
#image_ref = ubuntu-vm-heat-cfntools
#keypair_name = heat_key
[dashboard]
dashboard_url = http://172.16.200.130/
login_url = http://172.16.200.130/auth/login/
[scenario]
img_dir = /home/stack/devstack/files/images/cirros-0.3.1-x86_64-uec
ami_img_file = cirros-0.3.1-x86_64-blank.img
ari_img_file = cirros-0.3.1-x86_64-initrd
aki_img_file = cirros-0.3.1-x86_64-vmlinuz
ssh_user = cirros
large_ops_number = 0
[cli]
enabled = True
cli_dir = /usr/local/bin
timeout = 15
[service_available]
cinder = True
neutron = False
glance = True
swift = False
nova = True
heat = False
horizon = True
[stress]
max_instances = 32
log_check_interval = 60
default_thread_number_per_action=4

View File

@ -16,4 +16,5 @@ psycopg2==2.5
pyOpenSSL==0.13
pycrypto==2.6
python-openid==2.2.5
requests==1.2.3
requests==1.2.3
python-keystoneclient