├── test-requirements.txt ├── requirements.txt ├── .coveragerc ├── .gitignore ├── examples ├── facecat │ ├── media │ │ └── cat.png │ ├── static │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── css │ │ │ └── facecat.css │ │ └── js │ │ │ └── bootstrap.min.js │ ├── README.md │ ├── views │ │ ├── all.html │ │ ├── show.html │ │ └── index.html │ └── facecat.py ├── walkthrough │ ├── README.md │ └── walkthrough.py └── README.md ├── .travis.yml ├── CHANGES.md ├── doc └── source │ ├── index.rst │ ├── runabove.rst │ ├── examples.txt │ └── conf.py ├── LICENSE ├── runabove ├── tests │ ├── __init__.py │ ├── region.py │ ├── flavor.py │ ├── image.py │ ├── account.py │ ├── client.py │ ├── ssh_key.py │ ├── wrapper_api.py │ ├── instance.py │ └── storage.py ├── __init__.py ├── exception.py ├── image.py ├── region.py ├── base.py ├── flavor.py ├── client.py ├── account.py ├── ssh_key.py ├── wrapper_api.py ├── instance.py └── storage.py ├── setup.py └── README.md /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | httpretty==0.8.3 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-swiftclient>=2.1.0 2 | requests>=1.1 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = runabove 3 | 4 | [report] 5 | omit = */tests/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | build/ 4 | dist/ 5 | python_runabove.egg-info/ 6 | venv/ 7 | .coverage 8 | -------------------------------------------------------------------------------- /examples/facecat/media/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasLM/python-runabove/master/examples/facecat/media/cat.png -------------------------------------------------------------------------------- /examples/facecat/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasLM/python-runabove/master/examples/facecat/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /examples/facecat/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasLM/python-runabove/master/examples/facecat/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /examples/facecat/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasLM/python-runabove/master/examples/facecat/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - "python setup.py install" 6 | - "pip install -r test-requirements.txt" 7 | - "pip install coverage" 8 | - "pip install coveralls" 9 | script: coverage run setup.py test 10 | after_success: 11 | coveralls 12 | -------------------------------------------------------------------------------- /examples/walkthrough/README.md: -------------------------------------------------------------------------------- 1 | Walkthrough 2 | =========== 3 | 4 | This is an interactive Python script presenting RunAbove SDK. It shows how to 5 | use: 6 | 7 | * Instances 8 | * Regions 9 | * Flavors 10 | * Images 11 | * SSH keys 12 | 13 | To launch the script install RunAbove SDK and just enter: 14 | ```bash 15 | python walkthrough.py 16 | ``` 17 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changelog of Python SDK for RunAbove API 2 | ======================================== 3 | 4 | Release 1.1.0 (2014-07-30) 5 | -------------------------- 6 | 7 | * Limit the scope of permissions required by an application 8 | * Redirect the user to an URL after signing in 9 | 10 | 11 | Release 1.0.0 (2014-07-09) 12 | -------------------------- 13 | 14 | * Initial release 15 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | RunAbove Python SDK examples 2 | ============================ 3 | 4 | This folder contains demo applications that uses the Python SDK for the 5 | RunAbove API. 6 | 7 | Facecat 8 | ------- 9 | 10 | A small image hosting web application that uses Object Storage as backend for 11 | images. 12 | 13 | Walkthrough 14 | ----------- 15 | 16 | A Python script that shows how to list instances, containers, SSH keys and more 17 | as well as creating and deleting instances. 18 | -------------------------------------------------------------------------------- /examples/facecat/static/css/facecat.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 20px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | .navbar { 7 | margin-bottom: 20px; 8 | } 9 | 10 | .center-block { 11 | float: none; 12 | } 13 | 14 | span.glyphicon-big { 15 | font-size: 1.2em; 16 | } 17 | 18 | .btn-file { 19 | position: relative; 20 | overflow: hidden; 21 | } 22 | 23 | .btn-file input[type=file] { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | min-width: 100%; 28 | min-height: 100%; 29 | font-size: 999px; 30 | text-align: right; 31 | filter: alpha(opacity=0); 32 | opacity: 0; 33 | outline: none; 34 | background: white; 35 | cursor: inherit; 36 | display: block; 37 | } 38 | 39 | input[readonly] { 40 | background-color: white !important; 41 | cursor: text !important; 42 | } 43 | 44 | #submit { 45 | margin-top: 5px; 46 | } 47 | 48 | .padded { 49 | padding: 30px; 50 | } 51 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. runabove documentation master file, created by 2 | sphinx-quickstart on Mon Apr 28 14:12:38 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Python bindings to RunAbove API 7 | ==================================== 8 | 9 | RunAbove client is official client of RunAbove API. 10 | You can manage your instances, your object storage and your account with this client. 11 | 12 | For more information about our API check: 13 | https://community.runabove.com/knowledge-base/article/how-to-use-runabove-api 14 | 15 | 16 | Documentation 17 | ============= 18 | .. automodule:: runabove 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | runabove 23 | 24 | Examples 25 | ======== 26 | .. include:: examples.txt 27 | 28 | 29 | Indices and tables 30 | ================== 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /doc/source/runabove.rst: -------------------------------------------------------------------------------- 1 | Runabove 2 | ======== 3 | 4 | Account 5 | ------------------ 6 | 7 | .. automodule:: runabove.account 8 | :members: 9 | 10 | Client 11 | ------------------ 12 | 13 | .. automodule:: runabove.client 14 | :members: 15 | 16 | Exception 17 | ------------------ 18 | 19 | .. automodule:: runabove.exception 20 | :members: 21 | 22 | Flavor 23 | ------------------ 24 | 25 | .. automodule:: runabove.flavor 26 | :members: 27 | 28 | Image 29 | ------------------ 30 | 31 | .. automodule:: runabove.image 32 | :members: 33 | 34 | Instance 35 | ------------------ 36 | 37 | .. automodule:: runabove.instance 38 | :members: 39 | 40 | Region 41 | ------------------ 42 | 43 | .. automodule:: runabove.region 44 | :members: 45 | 46 | Ssh_key 47 | ------------------ 48 | 49 | .. automodule:: runabove.ssh_key 50 | :members: 51 | 52 | Storage 53 | ------------------ 54 | 55 | .. automodule:: runabove.storage 56 | :members: 57 | 58 | Wrapper_api 59 | -------------------- 60 | 61 | .. automodule:: runabove.wrapper_api 62 | :members: 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, OVH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | Except as contained in this notice, the name of OVH and or its trademarks (and 22 | among others RunAbove) shall not be used in advertising or otherwise to promote 23 | the sale, use or other dealings in this Software without prior written 24 | authorization from OVH. 25 | -------------------------------------------------------------------------------- /examples/facecat/README.md: -------------------------------------------------------------------------------- 1 | Facecat 2 | ======= 3 | 4 | This is an example application of RunAbove Object Storage with the Python SDK. 5 | It uses the bottle web framework and OpenCV to make a funny picture hosting 6 | webapp. Images are stored in RunAbove Object Storage and served to clients 7 | directly from there. 8 | 9 | To allow clients to download objects directly, the container is made public. 10 | 11 | How to install it on Debian/Ubuntu? 12 | -------------------------------- 13 | 14 | First make sure that you installed the RunAbove Python SDK on your machine. 15 | Then you can install the requirements used by the example application. 16 | 17 | The application requires OpenCV to apply modifications to the stored images. 18 | OpenCV and its Python bindings are available from repositories in Debian and 19 | Ubuntu. 20 | 21 | ```bash 22 | apt-get install libjpeg-dev python-dev python-opencv 23 | pip install bottle pillow 24 | ``` 25 | 26 | If you want to get the application working quickly without installing OpenCV on 27 | your computer you can install it on a Debian instance in RunAbove. 28 | 29 | In the file `facecat.py` you must put your application key, application secret 30 | and consumer key which are the credentials needed to access your RunAbove 31 | account from the API. 32 | 33 | You can launch the web server and access it with your browser: 34 | 35 | ```bash 36 | python facecat.py 37 | ``` 38 | -------------------------------------------------------------------------------- /runabove/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | -------------------------------------------------------------------------------- /runabove/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove bindings.""" 29 | import pkg_resources 30 | 31 | from client import Runabove 32 | 33 | __version__ = pkg_resources.get_distribution("python-runabove").version 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014, OVH 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # Except as contained in this notice, the name of OVH and or its trademarks 25 | # (and among others RunAbove) shall not be used in advertising or otherwise to 26 | # promote the sale, use or other dealings in this Software without prior 27 | # written authorization from OVH. 28 | 29 | import setuptools 30 | 31 | setuptools.setup( 32 | name='python-runabove', 33 | version='1.1.0', 34 | author='RunAbove', 35 | author_email='dev@runabove.com', 36 | url='https://www.runabove.com/', 37 | description='RunAbove API Client Library', 38 | keywords = "runabove api sdk", 39 | license='MIT', 40 | packages=['runabove'], 41 | test_suite='runabove.tests', 42 | install_requires=[ 43 | 'python-swiftclient>=2.1.0', 44 | 'requests>=1.1' 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /runabove/exception.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """This module defines the exceptions used in the SDK.""" 29 | 30 | 31 | class APIError(Exception): 32 | """General error of the API.""" 33 | 34 | def __init__(self, msg=None): 35 | Exception.__init__(self, msg) 36 | 37 | 38 | class ReadOnlyException(APIError): 39 | """Error raised when a read only data is modified""" 40 | pass 41 | 42 | 43 | class ResourceNotFoundError(APIError): 44 | """Error raised when a requested resource does not exist.""" 45 | pass 46 | 47 | 48 | class BadParametersError(APIError): 49 | """Error raised when a request contains bad parameters""" 50 | pass 51 | 52 | 53 | class ResourceAlreadyExistsError(APIError): 54 | """Error raised when trying to create a resource that exists.""" 55 | pass 56 | 57 | 58 | class NetworkError(APIError): 59 | """Error raised when there is an error from network layer""" 60 | pass 61 | -------------------------------------------------------------------------------- /examples/facecat/views/all.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Generated pictures - Facecat 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 43 | 44 | 45 | 46 |
47 |

Generated pictures

48 | % for obj in objects: 49 |
50 | Facecatted {{obj.name}} 51 |
52 | % end 53 |
54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /runabove/image.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove image service library.""" 29 | 30 | from base import Resource, BaseManagerWithList 31 | 32 | 33 | class ImageManager(BaseManagerWithList): 34 | """Manage images available in RunAbove.""" 35 | 36 | basepath = '/image' 37 | 38 | def get_by_id(self, image_id=None): 39 | """Get one image from a RunAbove account. 40 | 41 | :param image_id: ID of the image to retrieve 42 | """ 43 | url = self.basepath + '/' + self._api.encode_for_api(image_id) 44 | image = self._api.get(url) 45 | return self._dict_to_obj(image) 46 | 47 | def _dict_to_obj(self, key): 48 | """Converts a dict to an image object.""" 49 | region = self._handler.regions._name_to_obj(key['region']) 50 | return Image(self, 51 | key['id'], 52 | key.get('name'), 53 | region=region) 54 | 55 | 56 | class Image(Resource): 57 | """Represents one image.""" 58 | 59 | def __init__(self, manager, id, name, region): 60 | self._manager = manager 61 | self.id = id 62 | self.name = name 63 | self.region = region 64 | 65 | -------------------------------------------------------------------------------- /examples/facecat/views/show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Picture - Facecat 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 43 | 44 | 45 | 46 |
47 |

And the result is…

48 |
49 | Facecatted 50 |
51 |
52 |
53 | Download 54 | Delete 55 |
56 |
57 |
58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /runabove/region.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove region service library.""" 29 | 30 | from base import Resource, BaseManager 31 | from .exception import ResourceNotFoundError 32 | 33 | 34 | class RegionManager(BaseManager): 35 | """Manage regions available in RunAbove.""" 36 | 37 | basepath = '/region' 38 | 39 | def list(self): 40 | """Get list of regions available.""" 41 | 42 | res = self._api.get(self.basepath) 43 | regions = [] 44 | for region_name in res: 45 | regions.append(Region(self, region_name)) 46 | return regions 47 | 48 | def _name_to_obj(self, region_name): 49 | """Makes a region object by a name. 50 | 51 | It does not check if the region actually exists. 52 | """ 53 | return Region(self, region_name) 54 | 55 | def get_by_name(self, region_name): 56 | """Get a region by its name. 57 | 58 | :param region_name: Name of the region to retrieve 59 | :raises ResourceNotFoundError: Region does not exist 60 | """ 61 | regions = self.list() 62 | for region in regions: 63 | if region.name == region_name: 64 | return region 65 | raise ResourceNotFoundError(msg='Region %s does not exist' 66 | % region_name) 67 | 68 | 69 | class Region(Resource): 70 | """Represents one region.""" 71 | 72 | def __init__(self, manager, name): 73 | self._manager = manager 74 | self.name = name 75 | -------------------------------------------------------------------------------- /runabove/base.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove base class definition library.""" 29 | 30 | 31 | class BaseManager(object): 32 | """Basic manager type providing common operations. 33 | 34 | Managers interact with a particular type ressource (instances, 35 | images, etc.) and provide CRUD operations for them. 36 | """ 37 | def __init__(self, wrapper_api, runabove_handler): 38 | """Build a manager with reference to the API. 39 | 40 | :param wrapper_api: client of RunAbove API 41 | :param runabove_handler: reference to RunAbove user interface 42 | """ 43 | self._api = wrapper_api 44 | self._handler = runabove_handler 45 | 46 | class BaseManagerWithList(BaseManager): 47 | """Manager with list and list_by_region methods.""" 48 | 49 | def list(self): 50 | """Get a list of objects in an account.""" 51 | objs = [] 52 | for obj in self._api.get(self.basepath): 53 | objs.append(self._dict_to_obj(obj)) 54 | return objs 55 | 56 | def list_by_region(self, region): 57 | """Get a list of objects in a region.""" 58 | try: 59 | region_name = region.name 60 | except AttributeError: 61 | region_name = region 62 | content = {'region': region_name} 63 | objs = [] 64 | for obj in self._api.get(self.basepath, content): 65 | objs.append(self._dict_to_obj(obj)) 66 | return objs 67 | 68 | 69 | 70 | class Resource(object): 71 | """Base class for resource (obj, flavor, etc.).""" 72 | 73 | -------------------------------------------------------------------------------- /runabove/flavor.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove flavor service library.""" 29 | 30 | from base import Resource, BaseManagerWithList 31 | from .exception import ResourceNotFoundError 32 | 33 | 34 | class FlavorManager(BaseManagerWithList): 35 | """Manage flavors available in RunAbove.""" 36 | 37 | basepath = '/flavor' 38 | 39 | def _dict_to_obj(self, flavor): 40 | """Converts a dict to a Flavor object.""" 41 | region = self._handler.regions._name_to_obj(flavor['region']) 42 | return Flavor(self, 43 | flavor['id'], 44 | flavor.get('disk'), 45 | flavor.get('name'), 46 | flavor.get('ram'), 47 | flavor.get('vcpus'), 48 | region) 49 | 50 | def get_by_id(self, flavor_id): 51 | """Get a flavor by its id. 52 | 53 | :param flavor_id: ID of the flavor to retrieve 54 | :raises ResourceNotFoundError: Flavor does not exist 55 | """ 56 | for flavor in self.list(): 57 | if flavor.id == flavor_id: 58 | return flavor 59 | raise ResourceNotFoundError(msg='Flavor %s does not exist' 60 | % flavor_id) 61 | 62 | 63 | class Flavor(Resource): 64 | """Represents one flavor.""" 65 | 66 | def __init__(self, manager, flavor_id, disk, 67 | name, ram, vcpus, region): 68 | self._manager = manager 69 | self.id = flavor_id 70 | self.disk = disk 71 | self.name = name 72 | self.ram = ram 73 | self.vcpus = vcpus 74 | self.region = region 75 | -------------------------------------------------------------------------------- /runabove/tests/region.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import runabove 29 | import unittest 30 | import mock 31 | import json 32 | 33 | class TestRegion(unittest.TestCase): 34 | 35 | answer_list = '["BHS-1", "SBG-1"]' 36 | 37 | @mock.patch('runabove.wrapper_api') 38 | @mock.patch('runabove.client') 39 | def setUp(self, mock_wrapper, mock_client): 40 | self.mock_wrapper = mock_wrapper 41 | self.regions = runabove.region.RegionManager(mock_wrapper, mock_client) 42 | 43 | def test_base_path(self): 44 | self.assertEquals(self.regions.basepath, '/region') 45 | 46 | def test_list(self): 47 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 48 | region_list = self.regions.list() 49 | self.mock_wrapper.get.assert_called_once_with(self.regions.basepath) 50 | self.assertIsInstance(region_list, list) 51 | self.assertTrue(len(region_list) > 0) 52 | for region in region_list: 53 | self.assertIsInstance(region, runabove.region.Region) 54 | 55 | def test_get_by_name(self): 56 | region_name = 'SBG-1' 57 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 58 | region = self.regions.get_by_name(region_name) 59 | self.mock_wrapper.get.assert_called_once_with(self.regions.basepath) 60 | self.assertIsInstance(region, runabove.region.Region) 61 | self.assertEquals(region.name, region_name) 62 | 63 | def test_get_by_name_404(self): 64 | with self.assertRaises(runabove.exception.ResourceNotFoundError): 65 | self.regions.get_by_name('RBX-404') 66 | 67 | if __name__ == '__main__': 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /doc/source/examples.txt: -------------------------------------------------------------------------------- 1 | To communicate with the API, each call made by your application must be signed 2 | and include the consumer key of the user. The signature process is 3 | automatically handled by the SDK. However if the user don't have a valid 4 | consumer key yet you can redirect him to RunAbove authentication page with the 5 | following code:: 6 | 7 | 8 | from runabove import Runabove 9 | 10 | application_key = 'your_app_key' 11 | application_secret = 'your_app_secret' 12 | 13 | # Create an instance of Runabove SDK interface 14 | run = Runabove(application_key, application_secret) 15 | 16 | # Request an URL to securely authenticate the user 17 | print "You should login here: %s" % run.get_login_url() 18 | raw_input("When you are logged, press Enter") 19 | 20 | # Show the consumer key 21 | print "Your consumer key is: %s" % run.get_consumer_key() 22 | 23 | How to manage instances? 24 | ------------------------ 25 | 26 | Launching an instance is easy. First get the flavor, image and region where you 27 | want your instance to be created and call `Runabove.instances.create()`. To 28 | delete an instance just call the `instance.delete()` method:: 29 | 30 | from runabove import Runabove 31 | 32 | application_key = 'your_app_key' 33 | application_secret = 'your_app_secret' 34 | consumer_key = 'your_consumer_key' 35 | 36 | # Create the Runabove SDK interface 37 | run = Runabove(application_key, 38 | application_secret, 39 | consumer_key=consumer_key) 40 | 41 | # Get a region, flavor and image 42 | region = run.regions.list().pop() 43 | flavor = run.flavors.list_by_region(region).pop() 44 | image = run.images.list_by_region(region).pop() 45 | 46 | # Launch a new instance 47 | instance = run.instances.create(region, 'My instance', flavor, image) 48 | 49 | # List instances 50 | print 'Instances:' 51 | for i in run.instances.list(): 52 | print ' - %s (%s)' % (i.name, i.image.name) 53 | 54 | # Delete the newly created instance 55 | instance.delete() 56 | print '%s deleted' % instance.name 57 | 58 | How to use storage? 59 | ------------------- 60 | :: 61 | 62 | from runabove import Runabove 63 | 64 | application_key = 'your_app_key' 65 | application_secret = 'your_app_secret' 66 | consumer_key = 'your_consumer_key' 67 | 68 | # Create an instance of Runabove SDK interface 69 | run = Runabove(application_key, 70 | application_secret, 71 | consumer_key=consumer_key) 72 | 73 | # Get a region available 74 | region = run.regions.list().pop() 75 | 76 | # Create a new container 77 | container_name = 'storage_test' 78 | container = run.containers.create(region, container_name) 79 | print "Storage container '%s' created" % container.name 80 | 81 | # Create a new object 82 | object_name = 'object.txt' 83 | container.create_object(object_name, 'This is the content') 84 | print "Object '%s' created" % object_name 85 | 86 | # List objects of the container 87 | print "Objects in '%s':" % container.name 88 | for obj in container.list_objects(): 89 | print " - %s (%d bytes)" % (obj.name, obj.size) 90 | 91 | # Delete the object 92 | obj.delete() 93 | print "Object '%s' deleted" % obj.name 94 | 95 | # Delete the container 96 | container.delete() 97 | print "Storage container '%s' deleted" % container.name 98 | -------------------------------------------------------------------------------- /runabove/tests/flavor.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import mock 30 | import json 31 | 32 | import runabove 33 | 34 | class TestFlavor(unittest.TestCase): 35 | 36 | answer_list = '''[ 37 | { 38 | "id": "4245b91e-d9cf-4c9d-a109-f6a32da8a5cc", 39 | "disk": 240, 40 | "name": "pci2.d.r1", 41 | "ram": 28672, 42 | "vcpus": 4, 43 | "region": "BHS-1" 44 | }, 45 | { 46 | "id": "ab35df0e-4632-48b2-b6a5-c1f1d922bd43", 47 | "disk": 240, 48 | "name": "pci2.d.c1", 49 | "ram": 16384, 50 | "vcpus": 6, 51 | "region": "BHS-1" 52 | } 53 | ]''' 54 | 55 | @mock.patch('runabove.wrapper_api') 56 | @mock.patch('runabove.client') 57 | def setUp(self, mock_wrapper, mock_client): 58 | self.mock_wrapper = mock_wrapper 59 | self.flavors = runabove.flavor.FlavorManager(mock_wrapper, mock_client) 60 | 61 | def test_base_path(self): 62 | self.assertEquals(self.flavors.basepath, '/flavor') 63 | 64 | def test_list(self): 65 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 66 | flavor_list = self.flavors.list() 67 | self.mock_wrapper.get.assert_called_once_with(self.flavors.basepath) 68 | for flavor in flavor_list: 69 | self.assertIsInstance(flavor, runabove.flavor.Flavor) 70 | 71 | def test_list_by_region(self): 72 | region_name = 'BHS-1' 73 | content = {'region': region_name} 74 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 75 | flavor_list = self.flavors.list_by_region(region_name) 76 | self.mock_wrapper.get.assert_called_once_with( 77 | self.flavors.basepath, 78 | content 79 | ) 80 | for flavor in flavor_list: 81 | self.assertIsInstance(flavor, runabove.flavor.Flavor) 82 | 83 | def test_get_by_id(self): 84 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 85 | f = self.flavors.get_by_id('ab35df0e-4632-48b2-b6a5-c1f1d922bd43') 86 | self.assertIsInstance(f, runabove.flavor.Flavor) 87 | 88 | def test_get_by_id_404(self): 89 | with self.assertRaises(runabove.exception.ResourceNotFoundError): 90 | self.flavors.get_by_id('40404040-4040-4040-4040-404040404040') 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /runabove/client.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove SDK interface for users.""" 29 | 30 | from wrapper_api import WrapperApi 31 | from flavor import FlavorManager 32 | from region import RegionManager 33 | from ssh_key import SSHKeyManager 34 | from image import ImageManager 35 | from instance import InstanceManager 36 | from storage import ContainerManager 37 | from account import AccountManager 38 | 39 | 40 | class Runabove(object): 41 | """SDK interface to get cloud services from RunAbove.""" 42 | 43 | access_rules = [ 44 | {'method': 'GET', 'path': '/*'}, 45 | {'method': 'POST', 'path': '/*'}, 46 | {'method': 'PUT', 'path': '/*'}, 47 | {'method': 'DELETE', 'path': '/*'} 48 | ] 49 | 50 | def __init__(self, application_key, application_secret, consumer_key=None): 51 | """Create the main interface of the SDK. 52 | 53 | :param application_key: key of your RunAbove api's application 54 | :param application_secret: password of your RunAbove api's application 55 | """ 56 | self._api = WrapperApi(application_key, 57 | application_secret, 58 | consumer_key) 59 | self.flavors = FlavorManager(self._api, self) 60 | self.regions = RegionManager(self._api, self) 61 | self.ssh_keys = SSHKeyManager(self._api, self) 62 | self.images = ImageManager(self._api, self) 63 | self.instances = InstanceManager(self._api, self) 64 | self.account = AccountManager(self._api, self) 65 | self.containers = ContainerManager(self._api, self) 66 | 67 | def get_login_url(self, access_rules=None, redirect_url=None): 68 | """Get the URL to identify and login a customer. 69 | 70 | RunAbove API uses a remote connection to avoid storing passwords inside 71 | third party program. So the authentication is in two steps: 72 | First the app has to get a login URL and show it to the customer. 73 | Then, the customer must login with his account using this URL and the 74 | consumer key will be validated by the API. 75 | 76 | :param access_rules: List of access required by the application 77 | :param redirect_url: URL where user will be redirected after signin 78 | :raises ApiException: Error send by api 79 | """ 80 | if isinstance(access_rules, list): 81 | self.access_rules = access_rules 82 | credentials = self._api.request_credentials(self.access_rules, 83 | redirect_url) 84 | return credentials['validationUrl'] 85 | 86 | def get_consumer_key(self): 87 | """Get the current consumer key to communicate with the API.""" 88 | 89 | return self._api.consumer_key 90 | -------------------------------------------------------------------------------- /runabove/account.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove account service library.""" 29 | 30 | from base import Resource, BaseManager 31 | from .exception import ResourceNotFoundError 32 | 33 | 34 | class AccountManager(BaseManager): 35 | """Manage the account attached to the user.""" 36 | 37 | basepath = '/me' 38 | 39 | def get(self): 40 | """Get information about an account.""" 41 | res = self._api.get(self.basepath) 42 | return self._dict_to_obj(res) 43 | 44 | def _load_balance(self): 45 | """Loads information about balance. 46 | 47 | Sums total of all projects, so no usage per project. 48 | """ 49 | balance = self._api.get(self.basepath + '/balance') 50 | total_usage = 0 51 | for project in balance['currentUsages']: 52 | total_usage += project['currentTotal'] 53 | return (total_usage, balance['creditLeft']) 54 | 55 | def _dict_to_obj(self, key): 56 | """Converts a dict to an Account object.""" 57 | return Account(self, 58 | key.get('accountIdentifier'), 59 | key.get('firstname'), 60 | key.get('name'), 61 | key.get('address'), 62 | key.get('city'), 63 | key.get('postalCode'), 64 | key.get('area'), 65 | key.get('country'), 66 | key.get('email'), 67 | key.get('cellNumber')) 68 | 69 | 70 | class Account(Resource): 71 | """Represents one account.""" 72 | 73 | def __init__(self, manager, account_id, first_name, last_name, address, 74 | city, postal_code, area, country, email, phone): 75 | self._manager = manager 76 | self.account_id = account_id 77 | self.first_name = first_name 78 | self.last_name = last_name 79 | self.address = address 80 | self.city = city 81 | self.postal_code = postal_code 82 | self.area = area 83 | self.country = country 84 | self.email = email 85 | self.phone = phone 86 | self._current_total = None 87 | self._credit_left = None 88 | 89 | @property 90 | def current_total(self): 91 | """Lazy loading of balance information.""" 92 | if not self._current_total: 93 | self._current_total = self._manager._load_balance()[0] 94 | self._credit_left = self._manager._load_balance()[1] 95 | return self._current_total 96 | 97 | @property 98 | def credit_left(self): 99 | """Lazy loading of balance information.""" 100 | if not self._credit_left: 101 | self._current_total = self._manager._load_balance()[0] 102 | self._credit_left = self._manager._load_balance()[1] 103 | return self._credit_left 104 | -------------------------------------------------------------------------------- /runabove/ssh_key.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove SSH key service library.""" 29 | 30 | from base import Resource, BaseManagerWithList 31 | 32 | 33 | class SSHKeyManager(BaseManagerWithList): 34 | """Manage the SSH keys attached to an account.""" 35 | 36 | basepath = '/ssh' 37 | 38 | def get_by_name(self, region, name): 39 | """Get one SSH key from a RunAbove account. 40 | 41 | :param region: Region where the key is 42 | :param name: Name of the key to retrieve 43 | """ 44 | try: 45 | region_name = region.name 46 | except AttributeError: 47 | region_name = region 48 | url = self.basepath + '/' + self._api.encode_for_api(name) 49 | key = self._api.get(url, {'region': region_name}) 50 | return self._dict_to_obj(key) 51 | 52 | def _dict_to_obj(self, key): 53 | """Converts a dict to an SSHKey object.""" 54 | region = self._handler.regions._name_to_obj(key['region']) 55 | return SSHKey(self, 56 | key['name'], 57 | key.get('fingerPrint'), 58 | key.get('publicKey'), 59 | region) 60 | 61 | def create(self, region, name, public_key): 62 | """Register a new SSH key in a RunAbove account. 63 | 64 | :param region: Region where the key will be added 65 | :param name: Name of the key 66 | :param public_key: Public key value 67 | """ 68 | try: 69 | region_name = region.name 70 | except AttributeError: 71 | region_name = region 72 | content = { 73 | 'publicKey': public_key, 74 | 'region': region_name, 75 | 'name': name 76 | } 77 | self._api.post(self.basepath, content) 78 | return self.get_by_name(region_name, name) 79 | 80 | def delete(self, region, key): 81 | """Delete an SSH key from an account. 82 | 83 | :param region: Region where the key is 84 | :param key: SSH key to be deleted 85 | """ 86 | try: 87 | region_name = region.name 88 | except AttributeError: 89 | region_name = region 90 | try: 91 | name = key.name 92 | except AttributeError: 93 | name = key 94 | url = self.basepath + '/' + self._api.encode_for_api(name) 95 | return self._api.delete(url, {'region': region_name}) 96 | 97 | 98 | class SSHKey(Resource): 99 | """Represents one SSH key.""" 100 | 101 | def __init__(self, manager, name, finger_print, public_key, region): 102 | self._manager = manager 103 | self.name = name 104 | self.finger_print = finger_print 105 | self.public_key = public_key 106 | self.region = region 107 | 108 | def delete(self): 109 | """Delete the key from the account.""" 110 | self._manager.delete(self.region, self) 111 | -------------------------------------------------------------------------------- /runabove/tests/image.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import runabove 29 | import unittest 30 | import mock 31 | import json 32 | 33 | 34 | class TestImage(unittest.TestCase): 35 | 36 | answer_list = '''[ 37 | { 38 | "id": "fedora", 39 | "name": "Fedora 20", 40 | "region": "BHS-1" 41 | }, 42 | { 43 | "id": "centos", 44 | "name": "CentOS 6", 45 | "region": "BHS-1" 46 | } 47 | ]''' 48 | 49 | answer_one = '''{ 50 | "id": "Pfdq813FxcFel78954aFEfcpaW21", 51 | "name": "ra-snapshot", 52 | "status": "active", 53 | "creationDate": "2014-04-15T12:10:05Z", 54 | "minDisk": 240, 55 | "minRam": 0, 56 | "region": "BHS-1" 57 | }''' 58 | 59 | @mock.patch('runabove.wrapper_api') 60 | @mock.patch('runabove.client') 61 | def setUp(self, mock_wrapper, mock_client): 62 | self.mock_wrapper = mock_wrapper 63 | self.mock_client = mock_client 64 | self.mock_client.regions = runabove.region.RegionManager(mock_wrapper, 65 | mock_client) 66 | self.images = runabove.image.ImageManager(mock_wrapper, mock_client) 67 | 68 | def test_base_path(self): 69 | self.assertEquals(self.images.basepath, '/image') 70 | 71 | def test_list(self): 72 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 73 | image_list = self.images.list() 74 | self.mock_wrapper.get.assert_called_once_with(self.images.basepath) 75 | self.assertIsInstance(image_list, list) 76 | self.assertEquals(len(image_list), 2) 77 | for image in image_list: 78 | self.assertIsInstance(image, runabove.image.Image) 79 | 80 | def test_list_by_region(self): 81 | region_name = 'BHS-1' 82 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 83 | image_list = self.images.list_by_region(region_name) 84 | self.mock_wrapper.get.assert_called_once_with( 85 | self.images.basepath, 86 | {'region': region_name} 87 | ) 88 | self.assertIsInstance(image_list, list) 89 | self.assertEquals(len(image_list), 2) 90 | for image in image_list: 91 | self.assertIsInstance(image, runabove.image.Image) 92 | self.assertEquals(image.region.name, 'BHS-1') 93 | 94 | def test_find_by_image_id(self): 95 | the_id = "Pfdq813FxcFel78954aFEfcpaW21" 96 | self.mock_wrapper.get.return_value = json.loads(self.answer_one) 97 | image = self.images.get_by_id(the_id) 98 | self.mock_wrapper.get.assert_called_once_with( 99 | self.images.basepath + '/' +\ 100 | self.images._api.encode_for_api(the_id) 101 | ) 102 | self.assertIsInstance(image, runabove.image.Image) 103 | self.assertEquals(image.id, the_id) 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /runabove/tests/account.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import mock 30 | import json 31 | 32 | import runabove 33 | 34 | class TestAccount(unittest.TestCase): 35 | 36 | answer_account = '''{ 37 | "email": "test@runabove.com", 38 | "accountIdentifier": "test@runabove.com", 39 | "firstname": "Test", 40 | "country": "US", 41 | "city": null, 42 | "area": null, 43 | "cellNumber": "+99.999999999", 44 | "name": "Test", 45 | "address": null, 46 | "postalCode": null 47 | }''' 48 | 49 | answer_balance = '''{ 50 | "currentUsages": [ 51 | { 52 | "projectId": "randomlongstring", 53 | "currentTotal": 192.39 54 | }, 55 | { 56 | "projectId": "randomlongstring2", 57 | "currentTotal": 1 58 | } 59 | ], 60 | "creditLeft": 200 61 | }''' 62 | 63 | @mock.patch('runabove.wrapper_api') 64 | def setUp(self, mock_wrapper): 65 | self.mock_wrapper = mock_wrapper 66 | self.account = runabove.account.AccountManager(mock_wrapper, None) 67 | 68 | def test_base_path(self): 69 | self.assertEquals(self.account.basepath, '/me') 70 | 71 | def test_account_existance(self): 72 | self.mock_wrapper.get.return_value = json.loads(self.answer_account) 73 | account = self.account.get() 74 | self.assertIsInstance(account, runabove.account.Account) 75 | 76 | def test_load_balance(self): 77 | self.mock_wrapper.get.return_value = json.loads(self.answer_balance) 78 | balance = self.account._load_balance() 79 | self.assertIsInstance(balance, tuple) 80 | self.assertTrue(len(balance) == 2) 81 | self.assertEquals(balance[0], 193.39) 82 | self.assertEquals(balance[1], 200) 83 | 84 | 85 | class TestAccountObject(unittest.TestCase): 86 | 87 | @mock.patch('runabove.account.AccountManager') 88 | def setUp(self, mock_accounts): 89 | self.mock_accounts = mock_accounts 90 | self.account = runabove.account.Account( 91 | self.mock_accounts, 92 | 'test@runabove.com', 93 | 'Test', 94 | 'Test', 95 | None, 96 | None, 97 | None, 98 | None, 99 | 'US', 100 | 'test@runabove.com', 101 | '+99.999999999' 102 | ) 103 | 104 | def test_current_total(self): 105 | self.mock_accounts._load_balance.return_value = (193.39, 200) 106 | self.assertEquals(self.account.current_total, 193.39) 107 | self.mock_accounts._load_balance.assert_called_once() 108 | 109 | def test_credit_left(self): 110 | self.mock_accounts._load_balance.return_value = (193.39, 200) 111 | self.assertEquals(self.account.credit_left, 200) 112 | self.mock_accounts._load_balance.assert_called_once() 113 | 114 | if __name__ == '__main__': 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /examples/facecat/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Facecat 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 44 | 45 | 46 | 47 | % if 'error' in locals(): 48 |
An error happend while processing your request.
49 | % end 50 |
51 |

Upload a picture It's more fun if it's the picture of someone.

52 | 53 |
54 | 55 |
56 |
57 |
58 | 59 | 60 | Browse… 61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 | 76 |
77 | 78 | 79 |
80 | 81 | 82 | 83 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /runabove/tests/client.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import json 30 | import mock 31 | 32 | import runabove 33 | 34 | class TestRunabove(unittest.TestCase): 35 | 36 | application_key = 'test_apkey' 37 | application_secret = 'test_apsecret' 38 | consumer_key = 'test_conkey' 39 | access_rules = [ 40 | {'method': 'GET', 'path': '/*'}, 41 | {'method': 'POST', 'path': '/*'}, 42 | {'method': 'PUT', 'path': '/*'}, 43 | {'method': 'DELETE', 'path': '/*'} 44 | ] 45 | 46 | @mock.patch('runabove.wrapper_api') 47 | def setUp(self, mock_wrapper): 48 | self.mock_wrapper = mock_wrapper 49 | self.client = runabove.Runabove(self.application_key, 50 | self.application_secret, 51 | consumer_key=self.consumer_key) 52 | self.client._api = self.mock_wrapper 53 | 54 | def _get_login_url(self, access_rules=None, redirect_url=None): 55 | return_value = {"validationUrl": "runabove.com"} 56 | if not access_rules: 57 | access_rules = self.access_rules 58 | self.mock_wrapper.request_credentials.return_value = return_value 59 | login_url = self.client.get_login_url(access_rules, redirect_url) 60 | self.mock_wrapper.request_credentials.assert_called_once_with( 61 | access_rules, 62 | redirect_url 63 | ) 64 | self.assertEquals(login_url, return_value['validationUrl']) 65 | 66 | def test_get_login_url(self): 67 | self._get_login_url() 68 | 69 | def test_get_login_url_with_access_rules(self): 70 | access_rules = [ 71 | {'method': 'GET', 'path': '/me'} 72 | ] 73 | self._get_login_url(access_rules) 74 | 75 | def test_get_login_url_with_redirect(self): 76 | redirect_url = 'http://app.using.runabove.sdk.com/' 77 | self._get_login_url(redirect_url=redirect_url) 78 | 79 | def test_get_login_url_with_redirect_and_access_rules(self): 80 | redirect_url = 'http://app.using.runabove.sdk.com/' 81 | access_rules = [ 82 | {'method': 'GET', 'path': '/me'} 83 | ] 84 | self._get_login_url(access_rules, redirect_url) 85 | 86 | def test_get_consumer_key(self): 87 | self.mock_wrapper.consumer_key = self.consumer_key 88 | self.assertEquals(self.client.get_consumer_key(), 89 | self.consumer_key) 90 | 91 | def test_existance_of_flavors_manager(self): 92 | manager = self.client.flavors 93 | self.assertIsInstance(manager, runabove.flavor.FlavorManager) 94 | 95 | def test_existance_of_regions_manager(self): 96 | manager = self.client.regions 97 | self.assertIsInstance(manager, runabove.region.RegionManager) 98 | 99 | def test_existance_of_ssh_keys_manager(self): 100 | manager = self.client.ssh_keys 101 | self.assertIsInstance(manager, runabove.ssh_key.SSHKeyManager) 102 | 103 | def test_existance_of_images_manager(self): 104 | manager = self.client.images 105 | self.assertIsInstance(manager, runabove.image.ImageManager) 106 | 107 | def test_existance_of_instances_manager(self): 108 | manager = self.client.instances 109 | self.assertIsInstance(manager, runabove.instance.InstanceManager) 110 | 111 | def test_existance_of_storage_service(self): 112 | service = self.client.containers 113 | self.assertIsInstance(service, runabove.storage.ContainerManager) 114 | 115 | if __name__ == '__main__': 116 | unittest.main() 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python SDK for RunAbove API 2 | =========================== 3 | 4 | This is a Python SDK to use Instances and Object Storage on 5 | [RunAbove](https://www.runabove.com). The SDK uses the simple API provided by 6 | RunAbove. 7 | 8 | [![Build 9 | Status](https://travis-ci.org/runabove/python-runabove.svg?branch=master)](https://travis-ci.org/runabove/python-runabove) 10 | [![Coverage 11 | Status](https://coveralls.io/repos/runabove/python-runabove/badge.png?branch=master)](https://coveralls.io/r/runabove/python-runabove?branch=master) 12 | 13 | Quickstart 14 | ---------- 15 | 16 | The easiest way to start with the SDK is to install it from PyPi: 17 | 18 | pip install python-runabove 19 | 20 | RunAbove SDK can then be included in your Python programs. Some examples of 21 | applications using the SDK are available in the `examples` directory. 22 | 23 | Install from source 24 | ------------------- 25 | 26 | To install the SDK from the Github sources you have to clone the repository. 27 | Then, you can install the SDK with: 28 | 29 | python setup.py install 30 | 31 | 32 | Authenticate to RunAbove API 33 | ---------------------------- 34 | 35 | Each **application** that uses RunAbove API needs to be authenticated. For that 36 | you have to register your application, it is very easy and can be done at this 37 | address: https://api.runabove.com/createApp 38 | 39 | Then each **user** using your application will be securely authenticated with a 40 | consumer key. Thanks to this mecanism users don't need to give their plain text 41 | password to the application. The first time a user will use your application, 42 | he will be redirected to a web page where he can securely get his **consumer 43 | key**. 44 | 45 | How to get a consumer key with the SDK? 46 | --------------------------------------- 47 | 48 | To communicate with the API, each call made by your application must be signed 49 | and include the consumer key of the user. The signature process is 50 | automatically handled by the SDK. However if the user don't have a valid 51 | consumer key yet you can redirect him to RunAbove authentication page with the 52 | following code: 53 | 54 | ```python 55 | from runabove import Runabove 56 | 57 | application_key = 'your_app_key' 58 | application_secret = 'your_app_secret' 59 | 60 | # Create an instance of Runabove SDK interface 61 | run = Runabove(application_key, application_secret) 62 | 63 | # Request an URL to securely authenticate the user 64 | print "You should login here: %s" % run.get_login_url() 65 | raw_input("When you are logged, press Enter") 66 | 67 | # Show the consumer key 68 | print "Your consumer key is: %s" % run.get_consumer_key() 69 | ``` 70 | 71 | How to manage instances? 72 | ------------------------ 73 | 74 | Launching an instance is easy. First get the flavor, image and region where you 75 | want your instance to be created and call `Runabove.instances.create()`. To 76 | delete an instance just call the `instance.delete()` method: 77 | 78 | ```python 79 | from runabove import Runabove 80 | 81 | application_key = 'your_app_key' 82 | application_secret = 'your_app_secret' 83 | consumer_key = 'your_consumer_key' 84 | 85 | # Create the Runabove SDK interface 86 | run = Runabove(application_key, 87 | application_secret, 88 | consumer_key=consumer_key) 89 | 90 | # Get a region, flavor and image 91 | region = run.regions.list().pop() 92 | flavor = run.flavors.list_by_region(region).pop() 93 | image = run.images.list_by_region(region).pop() 94 | 95 | # Launch a new instance 96 | instance = run.instances.create(region, 'My instance', flavor, image) 97 | 98 | # List instances 99 | print 'Instances:' 100 | for i in run.instances.list(): 101 | print ' - %s (%s)' % (i.name, i.image.name) 102 | 103 | # Delete the newly created instance 104 | instance.delete() 105 | print '%s deleted' % instance.name 106 | ``` 107 | 108 | How to use storage? 109 | ------------------- 110 | 111 | ```python 112 | from runabove import Runabove 113 | 114 | application_key = 'your_app_key' 115 | application_secret = 'your_app_secret' 116 | consumer_key = 'your_consumer_key' 117 | 118 | # Create an instance of Runabove SDK interface 119 | run = Runabove(application_key, 120 | application_secret, 121 | consumer_key=consumer_key) 122 | 123 | # Get a region available 124 | region = run.regions.list().pop() 125 | 126 | # Create a new container 127 | container_name = 'storage_test' 128 | container = run.containers.create(region, container_name) 129 | print "Storage container '%s' created" % container.name 130 | 131 | # Create a new object 132 | object_name = 'object.txt' 133 | container.create_object(object_name, 'This is the content') 134 | print "Object '%s' created" % object_name 135 | 136 | # List objects of the container 137 | print "Objects in '%s':" % container.name 138 | for obj in container.list_objects(): 139 | print " - %s (%d bytes)" % (obj.name, obj.size) 140 | 141 | # Delete the object 142 | obj.delete() 143 | print "Object '%s' deleted" % obj.name 144 | 145 | # Delete the container 146 | container.delete() 147 | print "Storage container '%s' deleted" % container.name 148 | ``` 149 | 150 | How to build the documentation? 151 | ------------------------------- 152 | 153 | Documentation is based on sphinx. If you have not already installed sphinx, you 154 | can install it on your virtualenv: 155 | 156 | pip install sphinx 157 | 158 | To generate the documentation in the `doc/build` directory, it's possible to 159 | use directly: 160 | 161 | python setup.py build_sphinx 162 | 163 | How to run tests? 164 | ----------------- 165 | 166 | To run tests, you need to install some dependencies: 167 | 168 | pip install -r test-requirements.txt 169 | 170 | Then, you can directly run the unit tests 171 | 172 | python setup.py test 173 | 174 | License 175 | ------- 176 | 177 | The SDK code is released under a MIT style license, which means that it should 178 | be easy to integrate it to your application. 179 | Check the [LICENSE](LICENSE) file for more information. 180 | 181 | -------------------------------------------------------------------------------- /runabove/tests/ssh_key.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import runabove 29 | import unittest 30 | import mock 31 | import json 32 | 33 | 34 | class TestSshKey(unittest.TestCase): 35 | 36 | answer_list = '''[ 37 | { 38 | "publicKey": "ssh-rsa very-strong-key1 key-comment", 39 | "name": "TestKey1", 40 | "fingerPrint": "aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:a1", 41 | "region": "BHS-1" 42 | }, 43 | { 44 | "publicKey": "ssh-rsa very-strong-key2 key-comment", 45 | "name": "TestKey2", 46 | "fingerPrint": "aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:a2", 47 | "region": "BHS-1" 48 | } 49 | ]''' 50 | 51 | answer_one = '''{ 52 | "publicKey": "ssh-rsa very-strong-key1 key-comment", 53 | "name": "TestKey1", 54 | "fingerPrint": "aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:a1", 55 | "region": "BHS-1" 56 | }''' 57 | 58 | @mock.patch('runabove.wrapper_api') 59 | @mock.patch('runabove.client') 60 | def setUp(self, mock_wrapper, mock_client): 61 | self.mock_wrapper = mock_wrapper 62 | self.mock_client = mock_client 63 | self.mock_client.regions = runabove.region.RegionManager(mock_wrapper, 64 | mock_client) 65 | self.ssh_keys = runabove.ssh_key.SSHKeyManager(mock_wrapper, 66 | mock_client) 67 | 68 | def test_base_path(self): 69 | self.assertEquals(self.ssh_keys.basepath, '/ssh') 70 | 71 | def test_list(self): 72 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 73 | ssh_key_list = self.ssh_keys.list() 74 | self.mock_wrapper.get.assert_called_once_with( 75 | self.ssh_keys.basepath 76 | ) 77 | self.assertIsInstance(ssh_key_list, list) 78 | self.assertTrue(len(ssh_key_list) > 0) 79 | 80 | def test_list_by_region(self): 81 | region_name = 'BHS-1' 82 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 83 | ssh_key_list = self.ssh_keys.list_by_region(region_name) 84 | self.mock_wrapper.get.assert_called_once_with( 85 | self.ssh_keys.basepath, 86 | {'region': region_name} 87 | ) 88 | self.assertIsInstance(ssh_key_list, list) 89 | self.assertTrue(len(ssh_key_list) > 0) 90 | for ssh_key in ssh_key_list: 91 | self.assertIsInstance(ssh_key, runabove.ssh_key.SSHKey) 92 | self.assertEquals(ssh_key.region.name, region_name) 93 | 94 | def test_get_by_name(self): 95 | region_name = 'BHS-1' 96 | name = "TestKey1" 97 | self.mock_wrapper.encode_for_api.return_value = name 98 | self.mock_wrapper.get.return_value = json.loads(self.answer_one) 99 | ssh_key = self.ssh_keys.get_by_name(region_name, name) 100 | self.mock_wrapper.get.assert_called_once_with( 101 | self.ssh_keys.basepath + '/' + name, 102 | {'region': region_name} 103 | ) 104 | self.assertEquals(ssh_key.name, name) 105 | 106 | def test_create_ssh_key(self): 107 | region_name = 'BHS-1' 108 | name = "TestKey1" 109 | public_key = "ssh-rsa very-strong-key1 key-comment" 110 | content = { 111 | "name": name, 112 | "publicKey": public_key, 113 | "region": region_name 114 | } 115 | self.mock_wrapper.get.return_value = json.loads(self.answer_one) 116 | self.ssh_keys.create(region_name, name, public_key) 117 | self.mock_wrapper.post.assert_called_once_with( 118 | self.ssh_keys.basepath, content 119 | ) 120 | 121 | def test_delete(self): 122 | region_name = 'BHS-1' 123 | name = "TestKey1" 124 | self.mock_wrapper.encode_for_api.return_value = name 125 | self.ssh_keys.delete(region_name, name) 126 | self.mock_wrapper.delete.assert_called_once_with( 127 | self.ssh_keys.basepath + '/' + name, 128 | {'region': region_name} 129 | ) 130 | 131 | 132 | class TestSSHKeyObject(unittest.TestCase): 133 | 134 | @mock.patch('runabove.ssh_key.SSHKeyManager') 135 | def setUp(self, mock_ssh_keys): 136 | self.mock_ssh_keys = mock_ssh_keys 137 | self.ssh_key = runabove.ssh_key.SSHKey( 138 | self.mock_ssh_keys, 139 | 'MyTestKey', 140 | 'aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa', 141 | 'ssh-rsa very-strong-key key-comment', 142 | 'BHS-1' 143 | ) 144 | 145 | def test_delete_object(self): 146 | self.ssh_key.delete() 147 | self.mock_ssh_keys.delete.assert_called_once_with( 148 | self.ssh_key.region, 149 | self.ssh_key 150 | ) 151 | 152 | if __name__ == '__main__': 153 | unittest.main() 154 | -------------------------------------------------------------------------------- /examples/walkthrough/walkthrough.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014, OVH 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # Except as contained in this notice, the name of OVH and or its trademarks 25 | # (and among others RunAbove) shall not be used in advertising or otherwise to 26 | # promote the sale, use or other dealings in this Software without prior 27 | # written authorization from OVH. 28 | 29 | from __future__ import unicode_literals 30 | from os import path 31 | import sys 32 | import time 33 | 34 | from runabove import Runabove 35 | from runabove.exception import APIError 36 | 37 | # You can enter your crendentials here if you have them, 38 | # otherwise leave it empty to learn how to get them. 39 | application_key = None 40 | application_secret = None 41 | consumer_key = None 42 | 43 | def sizeof_fmt(num): 44 | """Display bytes as human readable.""" 45 | if num == 0: 46 | return '0 byte' 47 | for x in ['bytes','KB','MB','GB']: 48 | if num < 1024.0 and num > -1024.0: 49 | return "%3.1f %s" % (num, x) 50 | num /= 1024.0 51 | return "%3.1f %s" % (num, 'TB') 52 | 53 | def file_get_contents(filename): 54 | """Get the content of a file.""" 55 | with file(filename) as f: 56 | s = f.read() 57 | return s 58 | 59 | def pick_in_list(list_name, obj_list): 60 | """Generic function to ask the user to choose from a list.""" 61 | print '\n%ss available' % list_name 62 | for num, i in enumerate(obj_list): 63 | print '\t%d) %s' % (num+1, i.name) 64 | try: 65 | selected_num = raw_input('\nPlease select a %s number [1]: ' % 66 | list_name.lower()) 67 | selected_num = int(selected_num) - 1 68 | selected = obj_list[selected_num] 69 | except (ValueError, IndexError): 70 | selected = obj_list[0] 71 | print 'Using %s %s.' % (list_name.lower(), selected.name) 72 | return selected 73 | 74 | # Check if the user has application credentials 75 | if not application_key or not application_secret: 76 | print '\nTo use RunAbove SDK you need to register an application' 77 | choice = raw_input('Would you like to register one? (y/N): ') 78 | if choice.lower() != 'y': 79 | print 'Not creating an application, aborting' 80 | sys.exit(0) 81 | else: 82 | print '\nYou can do it here https://api.runabove.com/createApp' 83 | print 'When you are done enter here your Application Key and Secret' 84 | application_key = raw_input('\nApplication Key: ') 85 | application_secret = raw_input('Application Secret: ') 86 | 87 | # Create an instance of RunAbove SDK interface 88 | run = Runabove(application_key, 89 | application_secret, 90 | consumer_key=consumer_key) 91 | 92 | # Check if the user has a Consumer Key 93 | if not run.get_consumer_key(): 94 | print '\nEach user using your application needs a Consumer Key.' 95 | choice = raw_input('\nWould you like to get one? (y/N): ') 96 | if choice.lower() != 'y': 97 | print 'Not requesting a Consumer Key, aborting' 98 | sys.exit(0) 99 | else: 100 | print '\nYou can get it here %s' % run.get_login_url() 101 | raw_input('\nWhen you are logged, press Enter ') 102 | print 'Your consumer key is: %s' % run.get_consumer_key() 103 | 104 | # Get information about the account 105 | acc = run.account.get() 106 | print '\nHi %s,' % acc.first_name 107 | 108 | # Get the list of running instances 109 | instances = run.instances.list() 110 | print '\nYou have %d instance(s) running' % len(instances) 111 | for i in instances: 112 | print '\t- [%s] %s (%s, %s)' % (i.region.name, i.name, i.ip, i.image.name) 113 | 114 | # Get the list of containers 115 | containers = run.containers.list() 116 | print '\nYou have %d container(s)' % len(containers) 117 | for c in containers: 118 | if c.is_public: 119 | print '\t- [%s] %s (public, %s)' % (c.region.name, c.name, 120 | sizeof_fmt(c.size)) 121 | else: 122 | print '\t- [%s] %s (private, %s)' % (c.region.name, c.name, 123 | sizeof_fmt(c.size)) 124 | 125 | # Ask the user to select one region 126 | region = pick_in_list('Region', run.regions.list()) 127 | 128 | # Get the list of SSH keys in the selected region 129 | ssh_keys = run.ssh_keys.list_by_region(region) 130 | if ssh_keys: 131 | print '\nYou have %d SSH key(s) in %s' % (len(ssh_keys), region.name) 132 | for s in ssh_keys: 133 | print '\t- [%s] %s (%s)' % (s.region.name, s.name, s.finger_print) 134 | else: 135 | print '\nYou have no SSH key in %s' % region.name 136 | # Ask the user to create an SSH key 137 | choice = raw_input('\nWould you like to add one? (y/N): ' 138 | % region.name) 139 | if choice.lower() == 'y': 140 | ssh_key_path = path.expanduser('~/.ssh/id_rsa.pub') 141 | if not path.isfile(ssh_key_path): 142 | print 'You don\'t have a key in ~/.ssh/id_rsa.pub, aborting.' 143 | else: 144 | ssh_key_name = raw_input('Name of the SSH key: ') 145 | ssh_key_content = file_get_contents(ssh_key_path) 146 | try: 147 | run.ssh_keys.create(region, ssh_key_name, ssh_key_content) 148 | print 'Key added to %s' % region.name 149 | except APIError, e: 150 | print 'Couldn\'t add the SSH key to RunAbove. %s' % e 151 | 152 | # Ask the user to create an instance if he has a key 153 | if run.ssh_keys.list_by_region(region): 154 | choice = raw_input('\nWould you like to create an instance in %s? (y/N): ' 155 | % region.name) 156 | if choice.lower() == 'y': 157 | image = pick_in_list('Image', run.images.list_by_region(region)) 158 | flavor = pick_in_list('Flavor', run.flavors.list_by_region(region)) 159 | ssh_key = pick_in_list('SSH key', run.ssh_keys.list_by_region(region)) 160 | instance_name = raw_input('\nName of the instance: ') 161 | instance = run.instances.create(region, instance_name, 162 | flavor, image, ssh_key) 163 | print '\nInstance created' 164 | print 'Waiting for instance to be ready...' 165 | 166 | while not instance.status == 'ACTIVE': 167 | time.sleep(10) 168 | instance = run.instances.get_by_id(instance.id) 169 | 170 | print 'Instance launched' 171 | print '\t- IP: %s' % instance.ip 172 | print '\t- VNC: %s' % instance.vnc 173 | print '\t- SSH: ssh admin@%s' % instance.ip 174 | choice = raw_input('\nWould you like to delete the instance? (y/N): ') 175 | if choice.lower() == 'y': 176 | instance.delete() 177 | print 'Instance deleted' 178 | -------------------------------------------------------------------------------- /examples/facecat/facecat.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | from __future__ import unicode_literals 29 | from __future__ import division 30 | import os 31 | 32 | import cv2 33 | import cv2.cv as cv 34 | from PIL import Image 35 | from bottle import route, request, static_file,\ 36 | run as bottle_run, view, redirect 37 | 38 | from runabove import Runabove 39 | from runabove.exception import ResourceNotFoundError 40 | 41 | application_key = 'xxx' 42 | application_secret = 'xxx' 43 | consumer_key = 'xxx' 44 | container_name = 'facecat' 45 | 46 | # Create the Runabove SDK interface 47 | run = Runabove(application_key, 48 | application_secret, 49 | consumer_key=consumer_key) 50 | 51 | # Store our pictures in a random region 52 | region = run.regions.list().pop() 53 | 54 | # Create a container for our application 55 | container = run.containers.create(region, container_name) 56 | 57 | # Set the container public 58 | container.set_public() 59 | 60 | @route('/') 61 | @view('views/index.html') 62 | def index(): 63 | """Show the main page.""" 64 | return 65 | 66 | @route('/error') 67 | @view('views/index.html') 68 | def err(): 69 | """Show the main page with an error message.""" 70 | return {'error': True} 71 | 72 | @route('/static/') 73 | def callback(path): 74 | """Serve static files like CSS and JS.""" 75 | return static_file(path, root='static/') 76 | 77 | @route('/all') 78 | @view('views/all.html') 79 | def show_all(): 80 | """List all the objects in the container.""" 81 | objects = [] 82 | for stored_object in container.list_objects(): 83 | if stored_object.content_type == 'image/jpeg': 84 | objects.append(stored_object) 85 | return {'objects': objects} 86 | 87 | @route('/show/') 88 | @view('views/show.html') 89 | def show(name): 90 | """Show one object from the container.""" 91 | try: 92 | stored_object = container.get_object_by_name(name) 93 | except ResourceNotFoundError: 94 | redirect('/error') 95 | return {'obj': stored_object} 96 | 97 | @route('/delete/') 98 | def delete(name): 99 | """Delete an object from the container.""" 100 | container.delete_object(name) 101 | redirect('/all') 102 | 103 | @route('/upload', method='POST') 104 | @view('views/show.html') 105 | def upload(): 106 | """Cattify a picture and upload it to the container.""" 107 | # Check uploaded file 108 | upload = request.files.get('upload') 109 | if not upload: 110 | redirect('/error') 111 | name, ext = os.path.splitext(upload.filename) 112 | if ext.lower() not in ('.png', '.jpg', '.jpeg'): 113 | redirect('/error') 114 | 115 | # Create temporary directory for storing our files 116 | if not os.path.exists("/tmp/facecat"): 117 | os.makedirs("/tmp/facecat") 118 | 119 | # Save the uploaded picture to temporary directory 120 | upload.file.seek(0) 121 | file_path = "/tmp/facecat/" + upload.filename 122 | upload.save(file_path, True) 123 | 124 | # Cattify the picture ;) 125 | img = Cat(file_path) 126 | try: 127 | file_path = img.cattify() 128 | except IOError: 129 | os.remove(file_path) 130 | redirect('/error') 131 | path, file_name = os.path.split(file_path) 132 | 133 | # Upload the modified picture to the container 134 | new_object = container.create_object(file_name, 135 | open(file_path)) 136 | 137 | # Clean temporary file 138 | os.remove(file_path) 139 | 140 | return {'obj': new_object} 141 | 142 | 143 | class Cat(object): 144 | """Class that transforms people to cats.""" 145 | 146 | image_max_size = 700, 700 147 | 148 | def __init__(self, image_file): 149 | self.image_file = image_file 150 | self.original_file = image_file 151 | 152 | def cattify(self): 153 | """Apply the changes to the image.""" 154 | self.resize() 155 | self.transform() 156 | name, ext = os.path.splitext(self.original_file) 157 | final_name = name + '.jpeg' 158 | os.rename(self.image_file, final_name) 159 | return final_name 160 | 161 | def resize(self): 162 | """Resize the image as 'image_max_size' pixels.""" 163 | new_name = self.image_file + '.resized' 164 | im = Image.open(self.image_file) 165 | im.thumbnail(self.image_max_size, Image.ANTIALIAS) 166 | im.save(new_name, "JPEG") 167 | os.remove(self.image_file) 168 | self.image_file = new_name 169 | 170 | def transform(self): 171 | new_name = self.image_file + '.cat.jpeg' 172 | img_color = cv2.imread(self.image_file) 173 | overlay = cv2.imread("media/cat.png", -1) 174 | 175 | height, width, depth = overlay.shape 176 | img_gray = cv2.cvtColor(img_color, cv.CV_RGB2GRAY) 177 | img_gray = cv2.equalizeHist(img_gray) 178 | 179 | rects = self.detect(img_gray) 180 | img_out = img_color.copy() 181 | self.draw_img(img_out, rects, overlay, width, height) 182 | cv2.imwrite(new_name, img_out) 183 | os.remove(self.image_file) 184 | self.image_file = new_name 185 | 186 | def draw_img(self, img, rects, overlay, width, height): 187 | for x1, y1, x2, y2 in rects: 188 | xsize = x2 - x1 189 | ysize = y2 - y1 190 | fx=xsize/width 191 | fy=ysize/height 192 | x_offset= x1 193 | y_offset= y1 194 | 195 | s_img = cv2.resize(overlay, (0,0), fx=fx, fy=fy) 196 | for c in range(0,3): 197 | img[y_offset:y_offset+s_img.shape[0],\ 198 | x_offset:x_offset+s_img.shape[1], c] =\ 199 | s_img[:,:,c] * (s_img[:,:,3]/255.0) +\ 200 | img[y_offset:y_offset+s_img.shape[0],\ 201 | x_offset:x_offset+s_img.shape[1], c] *\ 202 | (1.0 - s_img[:,:,3]/255.0) 203 | 204 | def detect(self, img, cascade_fn='media/haarcascade_frontalface_alt.xml', 205 | scaleFactor=1.3, minNeighbors=4, minSize=(20, 20), 206 | flags=cv.CV_HAAR_SCALE_IMAGE): 207 | cascade = cv2.CascadeClassifier(cascade_fn) 208 | rects = cascade.detectMultiScale(img, 209 | scaleFactor=scaleFactor, 210 | minNeighbors=minNeighbors, 211 | minSize=minSize, 212 | flags=flags) 213 | if len(rects) == 0: 214 | return [] 215 | rects[:, 2:] += rects[:, :2] 216 | return rects 217 | 218 | 219 | if __name__ == '__main__': 220 | bottle_run(host='0.0.0.0', port=8080) 221 | -------------------------------------------------------------------------------- /runabove/wrapper_api.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """ 29 | This module provides a simple python wrapper over the RunAbove API 30 | It handles requesting credential, signing queries... 31 | """ 32 | 33 | import requests 34 | import hashlib 35 | import time 36 | import json 37 | import urllib 38 | 39 | from .exception import APIError, ResourceNotFoundError, BadParametersError, \ 40 | ResourceAlreadyExistsError, NetworkError 41 | 42 | 43 | class WrapperApi: 44 | """Simple wrapper class for RunAbove API.""" 45 | 46 | base_url = "https://api.runabove.com/1.0" 47 | 48 | def __init__(self, application_key, application_secret, consumer_key=None): 49 | """Construct a new wrapper instance. 50 | 51 | :param application_key: your application key given by RunAbove 52 | on application registration 53 | :param application_secret: your application secret given by RunAbove 54 | on application registration 55 | :param consumer_key: the consumer key you want to use, if any, 56 | given after a credential request 57 | """ 58 | self.application_key = application_key 59 | self.application_secret = application_secret 60 | self.consumer_key = consumer_key 61 | self._time_delta = None 62 | 63 | def time_delta(self): 64 | """Get the delta between this computer and RunAbove cluster.""" 65 | if self._time_delta is None: 66 | try: 67 | server_time = int(requests.get(self.base_url + "/time").text) 68 | except ValueError: 69 | raise APIError(msg='Impossible to get time from RunAbove') 70 | self._time_delta = server_time - int(time.time()) 71 | return self._time_delta 72 | 73 | def request_credentials(self, access_rules, redirect_url=None): 74 | """Request a Consumer Key to the API. 75 | 76 | That key will need to be validated with the link 77 | returned in the answer. 78 | 79 | :param access_rules: list of dictionaries listing the 80 | accesses your application will need. Each dictionary 81 | must contain two keys : method, of the four HTTP methods, 82 | and path, the path you will need access for, 83 | with * as a wildcard 84 | :param redirect_url: url where you want the user to be 85 | redirected to after he successfully validates 86 | the consumer key 87 | 88 | :raises APIError: Error send by api 89 | """ 90 | target_url = self.base_url + "/auth/credential" 91 | params = {"accessRules": access_rules} 92 | params["redirection"] = redirect_url 93 | query_data = json.dumps(params) 94 | q = requests.post( 95 | target_url, 96 | headers={ 97 | "X-Ra-Application": self.application_key, 98 | "Content-type": "application/json" 99 | }, 100 | data=query_data) 101 | res = json.loads(q.text) 102 | if q.status_code < 100 or q.status_code >= 300: 103 | raise APIError(res['message']) 104 | self.consumer_key = str(res['consumerKey']) 105 | return res 106 | 107 | def raw_call(self, method, path, content=None): 108 | """Sign a given query and return its result. 109 | 110 | :param method: the HTTP method of the request (get/post/put/delete) 111 | :param path: the url you want to request 112 | :param content: the object you want to send in your request 113 | (will be automatically serialized to JSON) 114 | 115 | :raises APIError: Error send by api 116 | """ 117 | target_url = self.base_url + path 118 | now = str(int(time.time()) + self.time_delta()) 119 | 120 | body = "" 121 | if content: 122 | body = json.dumps(content) 123 | 124 | if not self.consumer_key: 125 | raise BadParametersError(msg='Cannot call API without' 126 | 'Consumer Key') 127 | 128 | s1 = hashlib.sha1() 129 | s1.update( 130 | "+".join([ 131 | self.application_secret, 132 | self.consumer_key, 133 | method.upper(), 134 | target_url, 135 | body, 136 | now 137 | ])) 138 | sig = "$1$" + s1.hexdigest() 139 | query_headers = { 140 | "X-Ra-Application": self.application_key, 141 | "X-Ra-Timestamp": now, 142 | "X-Ra-Consumer": self.consumer_key, 143 | "X-Ra-Signature": sig, 144 | "Content-type": "application/json" 145 | } 146 | req = getattr(requests, method.lower()) 147 | result = req(target_url, headers=query_headers, data=body) 148 | 149 | if result.text: 150 | try: 151 | json_result = json.loads(result.text) 152 | except ValueError: 153 | raise APIError('API response is not valid') 154 | else: 155 | json_result = {} 156 | 157 | if result.status_code == 404: 158 | raise ResourceNotFoundError(msg=json_result.get('message')) 159 | if result.status_code == 400: 160 | raise BadParametersError(msg=json_result.get('message')) 161 | if result.status_code == 409: 162 | raise ResourceAlreadyExistsError(msg=json_result.get('message')) 163 | if result.status_code == 0: 164 | raise NetworkError() 165 | if result.status_code < 100 or result.status_code >= 300: 166 | raise APIError(msg=json_result.get('message')) 167 | 168 | return json_result 169 | 170 | def encode_for_api(self, string_to_encode): 171 | """Make sure the URI in correctly encoded. 172 | 173 | Runabove api need to encode "/" to %2F because slash 174 | are used into URI to distinct two ressources. 175 | 176 | :param string_to_encode: original string_to_encode 177 | """ 178 | return urllib.quote(string_to_encode).replace('/', '%2f') 179 | 180 | def get(self, path, content=None): 181 | """Helper method that wraps a GET call to raw_call. 182 | 183 | :param path: path ask inside api 184 | :param content: object content to send with this request 185 | 186 | :raises APIError: Error send by api 187 | """ 188 | return self.raw_call("get", path, content) 189 | 190 | def put(self, path, content): 191 | """Helper method that wraps a PUT call to raw_call. 192 | 193 | :param path: path ask inside api 194 | :param content: object content to send with this request 195 | 196 | :raises APIError: Error send by api 197 | """ 198 | return self.raw_call("put", path, content) 199 | 200 | def post(self, path, content): 201 | """Helper method that wraps a POST call to raw_call. 202 | 203 | :param path: path ask inside api 204 | :param content: object content to send with this request 205 | 206 | :raises APIError: Error send by api 207 | """ 208 | return self.raw_call("post", path, content) 209 | 210 | def delete(self, path, content=None): 211 | """Helper method that wraps a DELETE call to raw_call. 212 | 213 | :param path: path ask inside api 214 | :param content: object content to send with this request 215 | 216 | :raises APIError: Error send by api 217 | """ 218 | return self.raw_call("delete", path, content) 219 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # RunAbove documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 28 10:48:58 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import pkg_resources 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ['sphinx.ext.autodoc'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'RunAbove' 48 | copyright = u'2014, RunAbove team' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | release = pkg_resources.get_distribution("python-runabove").version 56 | version = release 57 | 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = [] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all 74 | # documents. 75 | #default_role = None 76 | 77 | # If true, '()' will be appended to :func: etc. cross-reference text. 78 | #add_function_parentheses = True 79 | 80 | # If true, the current module name will be prepended to all description 81 | # unit titles (such as .. function::). 82 | #add_module_names = True 83 | 84 | # If true, sectionauthor and moduleauthor directives will be shown in the 85 | # output. They are ignored by default. 86 | #show_authors = False 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = 'sphinx' 90 | 91 | # A list of ignored prefixes for module index sorting. 92 | #modindex_common_prefix = [] 93 | 94 | # If true, keep warnings as "system message" paragraphs in the built documents. 95 | #keep_warnings = False 96 | 97 | 98 | # -- Options for HTML output ---------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | html_theme = 'default' 103 | 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | #html_theme_options = {} 108 | 109 | # Add any paths that contain custom themes here, relative to this directory. 110 | #html_theme_path = [] 111 | 112 | # The name for this set of Sphinx documents. If None, it defaults to 113 | # " v documentation". 114 | #html_title = None 115 | 116 | # A shorter title for the navigation bar. Default is the same as html_title. 117 | #html_short_title = None 118 | 119 | # The name of an image file (relative to this directory) to place at the top 120 | # of the sidebar. 121 | #html_logo = None 122 | 123 | # The name of an image file (within the static path) to use as favicon of the 124 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 125 | # pixels large. 126 | #html_favicon = None 127 | 128 | # Add any paths that contain custom static files (such as style sheets) here, 129 | # relative to this directory. They are copied after the builtin static files, 130 | # so a file named "default.css" will overwrite the builtin "default.css". 131 | html_static_path = ['_static'] 132 | 133 | # Add any extra paths that contain custom files (such as robots.txt or 134 | # .htaccess) here, relative to this directory. These files are copied 135 | # directly to the root of the documentation. 136 | #html_extra_path = [] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | #html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | #html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | #html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'RunAbovedoc' 181 | 182 | 183 | # -- Options for LaTeX output --------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, 198 | # author, documentclass [howto, manual, or own class]). 199 | latex_documents = [ 200 | ('index', 'RunAbove.tex', u'RunAbove Documentation', 201 | u'RunAbove team', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # If true, show page references after internal links. 213 | #latex_show_pagerefs = False 214 | 215 | # If true, show URL addresses after external links. 216 | #latex_show_urls = False 217 | 218 | # Documents to append as an appendix to all manuals. 219 | #latex_appendices = [] 220 | 221 | # If false, no module index is generated. 222 | #latex_domain_indices = True 223 | 224 | 225 | # -- Options for manual page output --------------------------------------- 226 | 227 | # One entry per manual page. List of tuples 228 | # (source start file, name, description, authors, manual section). 229 | man_pages = [ 230 | ('index', 'runabove', u'RunAbove Documentation', 231 | [u'RunAbove team'], 1) 232 | ] 233 | 234 | # If true, show URL addresses after external links. 235 | #man_show_urls = False 236 | 237 | 238 | # -- Options for Texinfo output ------------------------------------------- 239 | 240 | # Grouping the document tree into Texinfo files. List of tuples 241 | # (source start file, target name, title, author, 242 | # dir menu entry, description, category) 243 | texinfo_documents = [ 244 | ('index', 'RunAbove', u'RunAbove Documentation', 245 | u'RunAbove team', 'RunAbove', 'One line description of project.', 246 | 'Miscellaneous'), 247 | ] 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #texinfo_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #texinfo_domain_indices = True 254 | 255 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 256 | #texinfo_show_urls = 'footnote' 257 | 258 | # If true, do not generate a @detailmenu in the "Top" node's menu. 259 | #texinfo_no_detailmenu = False 260 | -------------------------------------------------------------------------------- /runabove/instance.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove instance service library.""" 29 | 30 | from base import Resource, BaseManagerWithList 31 | 32 | 33 | class InstanceManager(BaseManagerWithList): 34 | """Manage instances for a RunAbove account.""" 35 | 36 | basepath = '/instance' 37 | 38 | def get_by_id(self, instance_id): 39 | """Get one instance from a RunAbove account. 40 | 41 | :param instance_id: ID of the instance to retrieve 42 | """ 43 | url = self.basepath + '/' + self._api.encode_for_api(instance_id) 44 | 45 | instance = self._api.get(url) 46 | return self._en_dict_to_obj(instance) 47 | 48 | def _load_vnc(self, instance): 49 | """Load the VNC link to an instance. 50 | 51 | :param instance: Instance to get VNC console from 52 | """ 53 | try: 54 | instance_id = instance.id 55 | except AttributeError: 56 | instance_id = instance 57 | url = self.basepath + '/' + instance_id + '/vnc' 58 | vnc = self._api.get(url) 59 | return vnc['url'] 60 | 61 | def _dict_to_obj(self, ins): 62 | """Converts a dict to an instance object.""" 63 | region = self._handler.regions._name_to_obj(ins['region']) 64 | return Instance(self, 65 | ins.get('instanceId'), 66 | ins.get('name'), 67 | ins.get('ip'), 68 | region, 69 | ins.get('flavorId'), 70 | ins.get('imageId'), 71 | ins.get('keyName'), 72 | ins.get('status'), 73 | ins.get('created')) 74 | 75 | def _en_dict_to_obj(self, ins): 76 | """Converts an enhanced dict to an instance object. 77 | 78 | The enhanced dict got with the GET of one instance allows 79 | to build the flavor, image and SSH key objects directly 80 | without making a call for each of them. However SSH key 81 | is not mandatory so can be None. 82 | """ 83 | try: 84 | ssh_key_name = ins['sshKey']['name'] 85 | ssh_key = self._handler.ssh_keys._dict_to_obj(ins['sshKey']) 86 | except TypeError: 87 | ssh_key_name = None 88 | ssh_key = None 89 | region = self._handler.regions._name_to_obj(ins['region']) 90 | flavor = self._handler.flavors._dict_to_obj(ins['flavor']) 91 | image = self._handler.images._dict_to_obj(ins['image']) 92 | return Instance(self, 93 | ins['instanceId'], 94 | ins.get('name'), 95 | ins.get('ipv4'), 96 | region, 97 | ins['flavor']['id'], 98 | ins['image']['id'], 99 | ssh_key_name, 100 | ins.get('status'), 101 | ins.get('created'), 102 | ips=ins.get('ips'), 103 | flavor=flavor, 104 | image=image, 105 | ssh_key=ssh_key) 106 | 107 | def create(self, region, name, flavor, image, ssh_key=None): 108 | """Launch a new instance inside a region with a public key. 109 | 110 | :param region: Name or object region of the new instance 111 | :param name: Name of the new instance 112 | :param flavor: ID or object flavor used for this Instance 113 | :param image: ID or object image for the instance 114 | :param ssh_key: Name or object SSH key to install 115 | """ 116 | try: 117 | region_name = region.name 118 | except AttributeError: 119 | region_name = region 120 | try: 121 | flavor_id = flavor.id 122 | except AttributeError: 123 | flavor_id = flavor 124 | try: 125 | image_id = image.id 126 | except AttributeError: 127 | image_id = image 128 | content = { 129 | 'flavorId': flavor_id, 130 | 'imageId': image_id, 131 | 'name': name, 132 | 'region': region_name 133 | } 134 | if ssh_key: 135 | try: 136 | content['sshKeyName'] = ssh_key.name 137 | except AttributeError: 138 | content['sshKeyName'] = ssh_key 139 | instance_id = self._api.post(self.basepath, content)['instanceId'] 140 | return self.get_by_id(instance_id) 141 | 142 | def rename(self, instance, new_name): 143 | """Rename an existing instance. 144 | 145 | :param instance: instance_id or Instance object to be deleted 146 | :param new_name: new name of instance 147 | """ 148 | content = { 149 | 'name': new_name 150 | } 151 | try: 152 | id = instance.id 153 | except AttributeError: 154 | id = instance 155 | url = self.basepath + '/' + self._api.encode_for_api(id) 156 | self._api.put(url, content) 157 | 158 | def delete(self, instance): 159 | """Delete an instance from an account. 160 | 161 | :param instance: instance_id or Instance object to be deleted 162 | """ 163 | try: 164 | id = instance.id 165 | except AttributeError: 166 | id = instance 167 | url = self.basepath + '/' + self._api.encode_for_api(id) 168 | self._api.delete(url) 169 | 170 | 171 | class Instance(Resource): 172 | """Represents one instance.""" 173 | 174 | def __init__(self, manager, id, name, ip, region, flavor_id, image_id, 175 | ssh_key_name, status, created, ips=None, 176 | flavor=None, image=None, ssh_key=None): 177 | self._manager = manager 178 | self.id = id 179 | self.name = name 180 | self.ip = ip 181 | self.created = created 182 | self.status = status 183 | self.region = region 184 | self._flavor_id = flavor_id 185 | self._flavor = flavor 186 | self._image_id = image_id 187 | self._image = image 188 | self._ssh_key_name = ssh_key_name 189 | self._ssh_key = ssh_key 190 | self._vnc = None 191 | self._ips = ips 192 | 193 | @property 194 | def flavor(self): 195 | """Lazy loading of flavor object.""" 196 | if not self._flavor: 197 | self._flavor = self._manager._handler.\ 198 | flavors.get_by_id(self._flavor_id) 199 | return self._flavor 200 | 201 | @property 202 | def image(self): 203 | """Lazy loading of image object.""" 204 | if not self._image: 205 | self._image = self._manager._handler.\ 206 | images.get_by_id(self._image_id) 207 | return self._image 208 | 209 | @property 210 | def ssh_key(self): 211 | """Lazy loading of ssh_key object.""" 212 | if not self._ssh_key_name: 213 | return None 214 | if not self._ssh_key: 215 | self._ssh_key = self._manager._handler.\ 216 | ssh_keys.get_by_name(self.region, self._ssh_key_name) 217 | return self._ssh_key 218 | 219 | @property 220 | def ips(self): 221 | """Lazy loading of the list of IPs.""" 222 | if self._ips is None: 223 | self._ips = self._manager.get_by_id(self.id)._ips 224 | return self._ips 225 | 226 | @property 227 | def vnc(self): 228 | """Lazy loading of VNC link.""" 229 | if not self._vnc: 230 | self._vnc = self._manager._load_vnc(self) 231 | return self._vnc 232 | 233 | def delete(self): 234 | """Delete instance represented by this object from the account.""" 235 | self._manager.delete(self) 236 | 237 | def rename(self, new_name): 238 | """Rename instance represented by this object.""" 239 | self._manager.rename(self, new_name) 240 | self.name = new_name 241 | -------------------------------------------------------------------------------- /runabove/tests/wrapper_api.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import time 30 | import json 31 | import hashlib 32 | 33 | import mock 34 | from mock import patch 35 | import httpretty 36 | from httpretty import register_uri, GET, POST, DELETE, PUT 37 | 38 | from runabove.wrapper_api import WrapperApi 39 | from runabove.exception import APIError, BadParametersError,\ 40 | ResourceNotFoundError, NetworkError,\ 41 | ResourceAlreadyExistsError 42 | 43 | class TestWrapperApi(unittest.TestCase): 44 | 45 | application_key = 'GpG8f61qtOcWmb0e' 46 | application_secret = 'fE42V43gAB5dpqURV8RPNq9U5rU1J8er' 47 | consumer_key = 'pW4Xn9s8MDpwfGD8s2DXpoXp3ESkCSY' 48 | base_url = 'https://api.runabove.com/1.0' 49 | fake_time = 1404395889.467238 50 | 51 | def setUp(self): 52 | self.time_patch = patch('time.time', return_value=self.fake_time) 53 | self.time_patch.start() 54 | httpretty.enable() 55 | self.api = WrapperApi(self.application_key, 56 | self.application_secret, 57 | self.consumer_key) 58 | self.api._time_delta = 0 59 | self.actual_base_url = WrapperApi.base_url 60 | 61 | def tearDown(self): 62 | httpretty.disable() 63 | httpretty.reset() 64 | self.time_patch.stop() 65 | 66 | def test_constructor(self): 67 | self.assertEquals(self.api.application_key, self.application_key) 68 | self.assertEquals(self.api.consumer_key, self.consumer_key) 69 | self.assertEquals(self.api.application_secret, self.application_secret) 70 | 71 | def test_base_url(self): 72 | self.assertEquals(self.api.base_url, self.base_url) 73 | 74 | def test_time_delta(self): 75 | self.api._time_delta = None 76 | fake_server_time = '1404395895' 77 | register_uri( 78 | GET, 79 | self.actual_base_url + '/time', 80 | body=fake_server_time 81 | ) 82 | time_delta = self.api.time_delta() 83 | self.assertEquals(time_delta, 6) 84 | self.assertEquals(self.api._time_delta, 6) 85 | 86 | def test_time_delta_with_bad_answer(self): 87 | self.api._time_delta = None 88 | fake_server_time = 'NotAnUTCTimestamp' 89 | register_uri( 90 | GET, 91 | self.actual_base_url + '/time', 92 | body=fake_server_time 93 | ) 94 | with self.assertRaises(APIError): 95 | self.api.time_delta() 96 | 97 | def _request_credentials(self, redirection=None, status=200): 98 | access_rules = [{'method': 'GET', 'path': '/storage'}] 99 | response = { 100 | 'validationUrl': 'https://api.runabove.com/login/qdfwb', 101 | 'consumerKey': '63C4VMs4MDpwfGDj4KQEnTjbkvjSJCSY', 102 | 'state': 'pendingTest' 103 | } 104 | if not status == 200: 105 | response = {'message': 'Error'} 106 | register_uri( 107 | POST, 108 | self.actual_base_url + '/auth/credential', 109 | content_type='application/json', 110 | status=status, 111 | body=json.dumps(response) 112 | ) 113 | res = self.api.request_credentials(access_rules, redirection) 114 | self.assertEquals(self.api.consumer_key, response['consumerKey']) 115 | self.assertEquals( 116 | httpretty.last_request().headers['content-type'], 117 | 'application/json' 118 | ) 119 | self.assertEquals( 120 | httpretty.last_request().headers['X-Ra-Application'], 121 | self.application_key 122 | ) 123 | self.assertEquals( 124 | httpretty.last_request().parsed_body['redirection'], 125 | redirection 126 | ) 127 | self.assertEquals( 128 | httpretty.last_request().parsed_body['accessRules'], 129 | access_rules 130 | ) 131 | 132 | def test_request_credentials_without_redirection(self): 133 | self._request_credentials() 134 | 135 | def test_request_credentials_with_redirection(self): 136 | self._request_credentials(redirection='http://test.net') 137 | 138 | def test_request_credentials_with_error(self): 139 | with self.assertRaises(APIError): 140 | self._request_credentials(status=500) 141 | 142 | def _raw_call(self, path='/test', method='get', content='', 143 | status=200, response=None): 144 | select_method = { 145 | 'get': GET, 146 | 'post': POST, 147 | 'delete' : DELETE, 148 | 'put': PUT 149 | } 150 | if not status == 200: 151 | response = {'message': 'Error'} 152 | body = '' 153 | if content: 154 | body = json.dumps(content) 155 | register_uri( 156 | select_method[method], 157 | self.actual_base_url + path, 158 | content_type='application/json', 159 | status=status, 160 | body=json.dumps(response) 161 | ) 162 | s1 = hashlib.sha1() 163 | s1.update( 164 | "+".join([ 165 | self.application_secret, 166 | self.consumer_key, 167 | method.upper(), 168 | self.base_url + path, 169 | body, 170 | '1404395889' 171 | ])) 172 | sig = "$1$" + s1.hexdigest() 173 | result = self.api.raw_call(method, path, content) 174 | self.assertEquals( 175 | httpretty.last_request().method, 176 | method.upper() 177 | ) 178 | self.assertEquals( 179 | httpretty.last_request().headers['content-type'], 180 | 'application/json' 181 | ) 182 | self.assertEquals( 183 | httpretty.last_request().headers['X-Ra-Application'], 184 | self.application_key 185 | ) 186 | self.assertEquals( 187 | httpretty.last_request().headers['X-Ra-Consumer'], 188 | self.consumer_key 189 | ) 190 | self.assertEquals( 191 | httpretty.last_request().headers['X-Ra-Timestamp'], 192 | '1404395889' 193 | ) 194 | self.assertEquals( 195 | httpretty.last_request().headers['X-Ra-Signature'], 196 | sig 197 | ) 198 | self.assertEquals(result, response) 199 | if content: 200 | self.assertEquals( 201 | httpretty.last_request().parsed_body, 202 | content 203 | ) 204 | 205 | def test_raw_call_without_consumer_key(self): 206 | self.api.consumer_key = None 207 | with self.assertRaises(BadParametersError): 208 | self._raw_call() 209 | 210 | def test_raw_call_with_error_404(self): 211 | with self.assertRaises(ResourceNotFoundError): 212 | self._raw_call(status=404) 213 | 214 | def test_raw_call_with_error_400(self): 215 | with self.assertRaises(BadParametersError): 216 | self._raw_call(status=400) 217 | 218 | def test_raw_call_with_error_409(self): 219 | with self.assertRaises(ResourceAlreadyExistsError): 220 | self._raw_call(status=409) 221 | 222 | def test_raw_call_with_error_500(self): 223 | with self.assertRaises(APIError): 224 | self._raw_call(status=500) 225 | 226 | def test_raw_call_get(self): 227 | self._raw_call() 228 | 229 | def test_raw_call_get_with_content(self): 230 | self._raw_call(content='{"test": 1}') 231 | 232 | def test_raw_call_get_with_response(self): 233 | self._raw_call(response='{"test": 1}') 234 | 235 | def test_raw_call_put(self): 236 | self._raw_call(method='put') 237 | 238 | def test_raw_call_put_with_content(self): 239 | self._raw_call(method='put', content='{"test": 1}') 240 | 241 | def test_raw_call_put_with_response(self): 242 | self._raw_call(method='put', response='{"test": 1}') 243 | 244 | def test_raw_call_post(self): 245 | self._raw_call(method='post') 246 | 247 | def test_raw_call_post_with_content(self): 248 | self._raw_call(method='post', content='{"test": 1}') 249 | 250 | def test_raw_call_post_with_response(self): 251 | self._raw_call(method='post', response='{"test": 1}') 252 | 253 | def test_raw_call_delete(self): 254 | self._raw_call(method='delete') 255 | 256 | def test_raw_call_delete_with_content(self): 257 | self._raw_call(method='delete', content='{"test": 1}') 258 | 259 | def test_raw_call_delete_with_response(self): 260 | self._raw_call(method='delete', response='{"test": 1}') 261 | 262 | def test_raw_call_with_invalid_answer(self): 263 | path = '/test' 264 | register_uri( 265 | GET, 266 | self.actual_base_url + path, 267 | content_type='application/json', 268 | body='Not A Valid "JSON" answer from API' 269 | ) 270 | with self.assertRaises(APIError): 271 | self.api.raw_call('get', path) 272 | 273 | def _external_call(self, method): 274 | patcher = mock.patch('runabove.wrapper_api.WrapperApi.raw_call') 275 | self.api.raw_call = patcher.start() 276 | select_method = { 277 | 'get': self.api.get, 278 | 'post': self.api.post, 279 | 'delete' : self.api.delete, 280 | 'put': self.api.put 281 | } 282 | select_method[method]('/test', None) 283 | self.api.raw_call.assert_called_once_with( 284 | method, 285 | '/test', 286 | None 287 | ) 288 | patcher.stop() 289 | 290 | def test_get(self): 291 | self._external_call('get') 292 | 293 | def test_post(self): 294 | self._external_call('post') 295 | 296 | def test_delete(self): 297 | self._external_call('delete') 298 | 299 | def test_put(self): 300 | self._external_call('put') 301 | 302 | def test_encode_for_api_without_modification(self): 303 | string = 'StringThatDoesNotNeedModification' 304 | result = self.api.encode_for_api(string) 305 | self.assertEquals(result, string) 306 | 307 | def test_encode_for_api_with_modification(self): 308 | string = 'String/That/Needs/Modification' 309 | expected = 'String%2fThat%2fNeeds%2fModification' 310 | result = self.api.encode_for_api(string) 311 | self.assertEquals(result, expected) 312 | 313 | if __name__ == '__main__': 314 | unittest.main() 315 | -------------------------------------------------------------------------------- /runabove/tests/instance.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import mock 30 | import json 31 | 32 | import runabove 33 | 34 | 35 | class TestInstance(unittest.TestCase): 36 | 37 | instance_id = '8c687d5d-a1c7-4670-aca8-65acfb23ab44' 38 | answer_list = '''[ 39 | { 40 | "instanceId": "8c687d5d-a1c7-4670-aca8-65acfb23ab44", 41 | "name": "Test1", 42 | "ip": "192.168.0.1", 43 | "flavorId": "ab35df0e-4632-48b2-b6a5-c1f1d922bd43", 44 | "imageId": "82a56d09-882d-48cc-82ce-eef59820879f", 45 | "keyName": "", 46 | "status": "ACTIVE", 47 | "created": "2014-06-01T09:13:15Z", 48 | "region": "BHS-1" 49 | }, 50 | { 51 | "instanceId": "6736e98e-d40c-408d-8198-8a20d21124f3", 52 | "name": "Test2", 53 | "ip": "192.168.0.1", 54 | "flavorId": "ab35df0e-4632-48b2-b6a5-c1f1d922bd43", 55 | "imageId": "6915107b-e40d-4fd7-95f5-5e2bd5c106d3", 56 | "keyName": "MyTestKey", 57 | "status": "ACTIVE", 58 | "created": "2014-06-20T10:10:38Z", 59 | "region": "BHS-1" 60 | } 61 | ]''' 62 | 63 | answer_one = '''{ 64 | "instanceId": "8c687d5d-a1c7-4670-aca8-65acfb23ab44", 65 | "name": "Test", 66 | "ipv4": "192.168.0.3", 67 | "created": "2014-06-01T09:13:15Z", 68 | "status": "ACTIVE", 69 | "flavor": { 70 | "id": "ab35df0e-4632-48b2-b6a5-c1f1d922bd43", 71 | "disk": 240, 72 | "name": "pci2.d.c1", 73 | "ram": 16384, 74 | "vcpus": 6, 75 | "region": "BHS-1" 76 | }, 77 | "image": { 78 | "id": "82a56d09-882d-48cc-82ce-eef59820879f", 79 | "name": "Debian 7", 80 | "region": "BHS-1" 81 | }, 82 | "sshKey": null, 83 | "region": "BHS-1" 84 | }''' 85 | 86 | answer_create_with_key = '''{ 87 | "instanceId": "8c687d5d-a1c7-4670-aca8-65acfb23ab44", 88 | "name": "Test", 89 | "ipv4": "", 90 | "created": "2014-07-02T14:02:39Z", 91 | "status": "BUILD", 92 | "flavor": { 93 | "id": "4245b91e-d9cf-4c9d-a109-f6a32da8a5cc", 94 | "disk": 240, 95 | "name": "pci2.d.r1", 96 | "ram": 28672, 97 | "vcpus": 4, 98 | "region": "BHS-1" 99 | }, 100 | "image": { 101 | "id": "82a56d09-882d-48cc-82ce-eef59820879f", 102 | "name": "Debian 7", 103 | "region": "BHS-1" 104 | }, 105 | "sshKey": { 106 | "publicKey": "ssh-rsa very-strong-key key-comment", 107 | "name": "MyTestKey", 108 | "fingerPrint": "aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa", 109 | "region": "BHS-1" 110 | }, 111 | "region": "BHS-1" 112 | }''' 113 | 114 | answer_create_without_key = '''{ 115 | "instanceId": "8c687d5d-a1c7-4670-aca8-65acfb23ab44", 116 | "name": "Test", 117 | "ipv4": "", 118 | "created": "2014-07-02T14:02:39Z", 119 | "status": "BUILD", 120 | "flavor": { 121 | "id": "4245b91e-d9cf-4c9d-a109-f6a32da8a5cc", 122 | "disk": 240, 123 | "name": "pci2.d.r1", 124 | "ram": 28672, 125 | "vcpus": 4, 126 | "region": "BHS-1" 127 | }, 128 | "image": { 129 | "id": "82a56d09-882d-48cc-82ce-eef59820879f", 130 | "name": "Debian 7", 131 | "region": "BHS-1" 132 | }, 133 | "sshKey": null, 134 | "region": "BHS-1" 135 | }''' 136 | 137 | @mock.patch('runabove.wrapper_api') 138 | @mock.patch('runabove.client') 139 | def setUp(self, mock_wrapper, mock_client): 140 | self.mock_wrapper = mock_wrapper 141 | self.instances = runabove.instance.InstanceManager(mock_wrapper, 142 | mock_client) 143 | 144 | def test_base_path(self): 145 | self.assertEquals(self.instances.basepath, '/instance') 146 | 147 | def test_list(self): 148 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 149 | instance_list = self.instances.list() 150 | self.mock_wrapper.get.assert_called_once_with( 151 | self.instances.basepath 152 | ) 153 | self.assertIsInstance(instance_list, list) 154 | self.assertTrue(len(instance_list) > 0) 155 | 156 | def test_get_by_id(self): 157 | self.mock_wrapper.encode_for_api.return_value = self.instance_id 158 | self.mock_wrapper.get.return_value = json.loads(self.answer_one) 159 | instance = self.instances.get_by_id(self.instance_id) 160 | self.mock_wrapper.get.assert_called_once_with( 161 | self.instances.basepath + '/' + self.instance_id 162 | ) 163 | self.assertIsInstance(instance, runabove.instance.Instance) 164 | 165 | def test_create_with_key(self): 166 | name = "Test" 167 | image_id = "82a56d09-882d-48cc-82ce-eef59820879f" 168 | flavor_id = "4245b91e-d9cf-4c9d-a109-f6a32da8a5cc" 169 | region_name = "BHS-1" 170 | public_key = "ssh-rsa very-strong-key key-comment" 171 | content = { 172 | 'flavorId': flavor_id, 173 | 'imageId': image_id, 174 | 'name': name, 175 | 'region': region_name, 176 | 'sshKeyName': public_key 177 | } 178 | self.mock_wrapper.post.return_value = json.loads( 179 | self.answer_create_with_key 180 | ) 181 | self.mock_wrapper.get.return_value = json.loads( 182 | self.answer_create_with_key 183 | ) 184 | self.mock_wrapper.encode_for_api.return_value = self.instance_id 185 | instance = self.instances.create( 186 | region_name, 187 | name, 188 | flavor_id, 189 | image_id, 190 | public_key 191 | ) 192 | self.mock_wrapper.post.assert_called_once_with( 193 | self.instances.basepath, 194 | content 195 | ) 196 | self.mock_wrapper.get.assert_called_once_with( 197 | self.instances.basepath + '/' + self.instance_id 198 | ) 199 | 200 | def test_create_without_key(self): 201 | name = "Test" 202 | image_id = "82a56d09-882d-48cc-82ce-eef59820879f" 203 | flavor_id = "4245b91e-d9cf-4c9d-a109-f6a32da8a5cc" 204 | region_name = "BHS-1" 205 | content = { 206 | 'flavorId': flavor_id, 207 | 'imageId': image_id, 208 | 'name': name, 209 | 'region': region_name 210 | } 211 | self.mock_wrapper.post.return_value = json.loads( 212 | self.answer_create_without_key 213 | ) 214 | self.mock_wrapper.get.return_value = json.loads( 215 | self.answer_create_without_key 216 | ) 217 | self.mock_wrapper.encode_for_api.return_value = self.instance_id 218 | self.instances.create( 219 | region_name, 220 | name, 221 | flavor_id, 222 | image_id 223 | ) 224 | self.mock_wrapper.post.assert_called_once_with( 225 | self.instances.basepath, 226 | content 227 | ) 228 | self.mock_wrapper.get.assert_called_once_with( 229 | self.instances.basepath + '/' + self.instance_id 230 | ) 231 | 232 | def test_rename_vm(self): 233 | name = 'MyTestInstanceWithNewName' 234 | self.mock_wrapper.encode_for_api.return_value = self.instance_id 235 | content = {"name": name} 236 | self.instances.rename(self.instance_id, name) 237 | self.mock_wrapper.put.assert_called_once_with( 238 | self.instances.basepath + '/' + self.instance_id, 239 | content 240 | ) 241 | 242 | def test_delete(self): 243 | self.mock_wrapper.encode_for_api.return_value = self.instance_id 244 | self.instances.delete(self.instance_id) 245 | self.mock_wrapper.delete.assert_called_once_with( 246 | self.instances.basepath + '/' + self.instance_id 247 | ) 248 | 249 | def test_load_vnc(self): 250 | url = "https://vnc-url" 251 | self.mock_wrapper.get.return_value = json.loads('''{ 252 | "type": "novnc", 253 | "url": "%s" 254 | }''' % url) 255 | vnc = self.instances._load_vnc(self.instance_id) 256 | self.mock_wrapper.get.assert_called_once_with( 257 | self.instances.basepath + '/' + self.instance_id + '/vnc' 258 | ) 259 | self.assertEquals(vnc, url) 260 | 261 | class TestInstanceObject(unittest.TestCase): 262 | 263 | @mock.patch('runabove.instance.InstanceManager') 264 | def setUp(self, mock_instances): 265 | self.mock_instances = mock_instances 266 | self.instance = runabove.instance.Instance( 267 | self.mock_instances, 268 | '9c687d5d-a1c7-4670-aca8-65acfb23ab44', 269 | 'MyTestInstance', 270 | '192.168.0.1', 271 | 'BHS-1', 272 | 'fc4c428d-c88b-4027-b35d-2ca176a8bd1a', 273 | 'b37437ea-e8de-474b-9628-54f563a3fd1e', 274 | 'MyTestKey', 275 | 'ACTIVE', 276 | '2014-07-01T09:13:15Z' 277 | ) 278 | 279 | def test_delete_object(self): 280 | self.instance.delete() 281 | self.mock_instances.delete.assert_called_once_with(self.instance) 282 | 283 | def test_rename_object(self): 284 | name = 'MyTestInstanceWithNewName' 285 | self.instance.rename(name) 286 | self.mock_instances.rename.assert_called_once_with(self.instance, name) 287 | 288 | def test_get_vnc_link(self): 289 | self.instance.vnc 290 | self.mock_instances.vnc.assert_called_once() 291 | 292 | def test_get_flavor(self): 293 | self.instance.flavor 294 | self.mock_instances._handler.flavors.get_by_id.assert_called_once_with( 295 | self.instance._flavor_id 296 | ) 297 | 298 | def test_get_image(self): 299 | self.instance.image 300 | self.mock_instances._handler.images.get_by_id.assert_called_once_with( 301 | self.instance._image_id 302 | ) 303 | 304 | def test_get_ssh_key(self): 305 | self.instance.ssh_key 306 | self.mock_instances._handler.ssh_keys.get_by_name.\ 307 | assert_called_once_with( 308 | self.instance.region, 309 | self.instance._ssh_key_name 310 | ) 311 | 312 | def test_get_ssh_key_empty(self): 313 | self.instance._ssh_key_name = None 314 | self.assertEquals(self.instance.ssh_key, None) 315 | 316 | def test_get_ips(self): 317 | self.instance.ips 318 | self.mock_instances.get_by_id.assert_called_once_with( 319 | self.instance.id 320 | ) 321 | 322 | 323 | if __name__ == '__main__': 324 | unittest.main() 325 | -------------------------------------------------------------------------------- /runabove/storage.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | """RunAbove Object Storage service library.""" 29 | 30 | import functools 31 | import mimetypes 32 | import urllib 33 | 34 | import swiftclient 35 | 36 | from base import Resource, BaseManagerWithList 37 | from .exception import APIError, ResourceNotFoundError 38 | 39 | 40 | class ContainerManager(BaseManagerWithList): 41 | """Manage containers available in RunAbove.""" 42 | 43 | basepath = '/storage' 44 | _swifts = {} 45 | 46 | def get_by_name(self, region, container_name): 47 | """Get a container by its name. 48 | 49 | As two containers with the same name can exist in two 50 | different regions we need to limit the search to one region. 51 | :param container_name: Name of the container to retrieve 52 | :raises ResourceNotFoundError: Container does not exist 53 | """ 54 | try: 55 | region_name = region.name 56 | except AttributeError: 57 | region_name = region 58 | content = {'region': region_name} 59 | url = self.basepath + '/' + self._api.encode_for_api(container_name) 60 | res = self._api.get(url, content) 61 | return self._en_dict_to_obj(res, region_name) 62 | 63 | def _dict_to_obj(self, container): 64 | """Converts a dict to a container object.""" 65 | region = self._handler.regions._name_to_obj(container['region']) 66 | return Container(self, 67 | container['name'], 68 | container.get('stored'), 69 | container.get('totalObjects'), 70 | region) 71 | 72 | def _en_dict_to_obj(self, container, region_name): 73 | """Converts a dict to a container object.""" 74 | region = self._handler.regions._name_to_obj(region_name) 75 | return Container(self, 76 | container['name'], 77 | container.get('stored'), 78 | container.get('totalObjects'), 79 | region, 80 | public=container.get('public')) 81 | 82 | def _get_endpoints(self): 83 | """Get the OpenStack endpoint for storage in each region.""" 84 | tokens = self._api.get('/token') 85 | catalog = tokens['token']['catalog'] 86 | for service in catalog: 87 | if service['type'] != 'object-store': 88 | continue 89 | return service['endpoints'], tokens['X-Auth-Token'] 90 | 91 | raise ResourceNotFoundError(msg='No Object Storage endpoint') 92 | 93 | def _get_swift_clients(self): 94 | """Get the swift client for each region.""" 95 | endpoints, token = self._get_endpoints() 96 | swifts = {} 97 | for endpoint in endpoints: 98 | s = swiftclient.client.Connection(preauthurl=endpoint['url'], 99 | preauthtoken=token) 100 | swifts[endpoint['region']] = { 101 | 'client': s, 102 | 'endpoint': endpoint['url'] 103 | } 104 | return swifts 105 | 106 | def _swift_call(self, region, action, *args, **kwargs): 107 | """Wrap calls to swiftclient to allow retry.""" 108 | try: 109 | region_name = region.name 110 | except AttributeError: 111 | region_name = region 112 | retries = 0 113 | while retries < 3: 114 | swift = self.swifts[region_name]['client'] 115 | call = getattr(swift, action.lower()) 116 | try: 117 | return call(*args, **kwargs) 118 | except swiftclient.exceptions.ClientException, e: 119 | if e.http_status == 401: 120 | # Token is invalid, regenerate swift clients 121 | self._swifts = None 122 | if e.http_status == 404: 123 | raise ResourceNotFoundError(msg=e.msg) 124 | else: 125 | raise e 126 | raise APIError(msg='Impossible to get a valid token') 127 | 128 | def create(self, region, container_name, public=False): 129 | """Create a new container in a region. 130 | 131 | :param region: Region where the container will be created 132 | :param container_name: Name of the container to create 133 | :param public: Make the containers public if True 134 | """ 135 | if public: 136 | headers = {'X-Container-Read': '.r:*,.rlistings'} 137 | else: 138 | headers = {} 139 | self._swift_call(region, 'put_container', 140 | container_name, headers=headers) 141 | return self.get_by_name(region, container_name) 142 | 143 | def delete(self, region, container): 144 | """Delete a container. 145 | 146 | :param region: Region where the container will be deleted 147 | :param container: Container to delete 148 | """ 149 | try: 150 | container_name = container.name 151 | except AttributeError: 152 | container_name = container 153 | self._swift_call(region, 'delete_container', container_name) 154 | 155 | def set_public(self, region, container, public=True): 156 | """Set a container publicly available. 157 | 158 | :param region: Region where the container is 159 | :param container: Container to make public 160 | :param public: Set container private if False 161 | """ 162 | try: 163 | container_name = container.name 164 | except AttributeError: 165 | container_name = container 166 | if public: 167 | headers = {'X-Container-Read': '.r:*,.rlistings'} 168 | else: 169 | headers = {'X-Container-Read': ''} 170 | self._swift_call(region, 'post_container', 171 | container_name, headers=headers) 172 | 173 | def set_private(self, region, container): 174 | """Set a container to private. 175 | 176 | :param region: Region where the container is 177 | :param container: Container to make private 178 | """ 179 | self.set_public(region, container, public=False) 180 | 181 | def get_region_url(self, region): 182 | """Get the URL endpoint for storage in a region. 183 | 184 | :param region: Region to get the endpoint for 185 | """ 186 | try: 187 | region_name = region.name 188 | except AttributeError: 189 | region_name = region 190 | try: 191 | return self.swifts[region_name]['endpoint'] 192 | except KeyError: 193 | raise ResourceNotFoundError(msg='Region does not exist') 194 | 195 | def copy_object(self, region, from_container, stored_object, 196 | to_container=None, new_object_name=None): 197 | """Copy an object from a container to another one. 198 | 199 | Containers must be in the same region. Both containers may be 200 | the same. Content-Type is read from the original object if 201 | available, otherwise it is guessed with file name, defaults to 202 | None if impossible to guess. 203 | 204 | :param region: Region where the containers are 205 | :param from_container: Container where the original object is 206 | :param stored_object: Object to copy 207 | :param to_container: Container where the object will be copied 208 | to. If None copy into the same container. 209 | :param new_object_name: Name of the new object. If None new name 210 | is taken from the original name. 211 | """ 212 | try: 213 | region_name = region.name 214 | except AttributeError: 215 | region_name = region 216 | try: 217 | from_container_name = from_container.name 218 | except AttributeError: 219 | from_container_name = from_container 220 | try: 221 | stored_object_name = stored_object.name 222 | content_type = stored_object.content_type 223 | except AttributeError: 224 | stored_object_name = stored_object 225 | content_type = mimetypes.guess_type(stored_object_name)[0] 226 | if to_container: 227 | try: 228 | to_container_name = to_container.name 229 | except AttributeError: 230 | to_container_name = to_container 231 | else: 232 | to_container_name = from_container_name 233 | if not new_object_name: 234 | new_object_name = stored_object_name 235 | original_location = '/%s/%s'%(from_container_name, stored_object_name) 236 | headers = {'X-Copy-From': original_location} 237 | self._swift_call(region_name, 238 | 'put_object', 239 | to_container_name, 240 | new_object_name, 241 | None, 242 | headers=headers, 243 | content_length=0, 244 | content_type=content_type) 245 | 246 | @property 247 | def swifts(self): 248 | """Lazy load of one swift client per region.""" 249 | if not self._swifts: 250 | self._swifts = self._get_swift_clients() 251 | return self._swifts 252 | 253 | class Container(Resource): 254 | """Represents one container.""" 255 | 256 | def __init__(self, manager, name, size, 257 | number_objects, region, public=None): 258 | self._manager = manager 259 | self.name = name 260 | self.size = size 261 | self.number_objects = number_objects 262 | self.region = region 263 | self._is_public = public 264 | 265 | @property 266 | def is_public(self): 267 | """Lazy loading of public state.""" 268 | if self._is_public is None: 269 | self._is_public = self._manager.\ 270 | get_by_name(self.region.name, self.name)._is_public 271 | return self._is_public 272 | 273 | def delete(self): 274 | """Delete the container.""" 275 | self._manager.delete(self.region, self) 276 | 277 | def _dict_to_obj(self, obj): 278 | """Converts a dict to a ObjectStored object.""" 279 | return ObjectStored(self, 280 | obj.get('name'), 281 | obj.get('bytes'), 282 | obj.get('last_modified'), 283 | obj.get('content_type')) 284 | 285 | def _en_dict_to_obj(self, name, info, data=None): 286 | """Converts a dict to a ObjectStored object.""" 287 | return ObjectStored(self, 288 | name, 289 | info.get('content-length'), 290 | info.get('last-modified'), 291 | info.get('content-type'), 292 | data=data) 293 | 294 | def list_objects(self): 295 | """List objects of a container.""" 296 | res = self._manager._swift_call(self.region.name, 297 | 'get_container', 298 | self.name, 299 | full_listing=True) 300 | objs = [] 301 | for obj in res[1]: 302 | objs.append(self._dict_to_obj(obj)) 303 | return objs 304 | 305 | def get_object_by_name(self, object_name, download=False): 306 | """Get an object stored by its name. 307 | 308 | Does not download the content of the object by default. 309 | :param object_name: Name of the object to create 310 | :param download: If True download also the object content 311 | """ 312 | if download: 313 | call = 'get_object' 314 | else : 315 | call = 'head_object' 316 | res = self._manager._swift_call(self.region.name, 317 | call, 318 | self.name, 319 | object_name) 320 | 321 | try: 322 | return self._en_dict_to_obj(object_name, res[0], data=res[1]) 323 | except KeyError: 324 | return self._en_dict_to_obj(object_name, res) 325 | 326 | def delete_object(self, object_stored): 327 | """Delete an object from a container. 328 | 329 | :param object_stored: the object to delete 330 | """ 331 | try: 332 | object_name = object_stored.name 333 | except AttributeError: 334 | object_name = object_stored 335 | self._manager._swift_call(self.region, 336 | 'delete_object', 337 | self.name, 338 | object_name) 339 | 340 | def create_object(self, object_name, content, content_type=None): 341 | """Upload an object to a container. 342 | 343 | :param object_name: Name of the object to create 344 | :param content: Content to upload, can be a string or a file-like 345 | object 346 | :param content_type: Content-type of the object, if None it will be 347 | computed with the extension of object name 348 | """ 349 | if not content_type: 350 | content_type = mimetypes.guess_type(object_name)[0] 351 | self._manager._swift_call(self.region.name, 352 | 'put_object', 353 | self.name, 354 | object_name, 355 | content, 356 | content_type=content_type) 357 | return self.get_object_by_name(object_name) 358 | 359 | def copy_object(self, stored_object, to_container=None, 360 | new_object_name=None): 361 | """Copy an object from a container to another one. 362 | 363 | Containers must be in the same region. Both containers may be 364 | the same. Content-Type is read from the original object if 365 | available, otherwise it is guessed with file name, defaults to 366 | None if impossible to guess. 367 | 368 | :param stored_object: Object to copy 369 | :param to_container: Container where the object will be copied 370 | to. If None copy into the same container. 371 | :param new_object_name: Name of the new object. If None new name 372 | is taken from the original name. 373 | """ 374 | self._manager.copy_object(self.region.name, self, stored_object, 375 | to_container, new_object_name) 376 | 377 | def set_public(self): 378 | """Set the container public.""" 379 | self._manager.set_public(self.region.name, self) 380 | 381 | def set_private(self): 382 | """Set the container private.""" 383 | self._manager.set_private(self.region.name, self) 384 | 385 | @property 386 | def url(self): 387 | """Get the URL to access a container.""" 388 | region_endpoint = self._manager.get_region_url(self.region.name) 389 | container_name = urllib.quote(self.name).replace('/', '%2f') 390 | return '%s/%s' % (region_endpoint, container_name) 391 | 392 | 393 | class ObjectStored(Resource): 394 | """Represents one swift object.""" 395 | 396 | def __init__(self, container, name, size, last_modified, 397 | content_type, data=None): 398 | self.container = container 399 | self.name = name 400 | self.size = size 401 | self.last_modified = last_modified 402 | self.content_type = content_type 403 | self._data = data 404 | 405 | @property 406 | def data(self): 407 | """Lazy loading of content of an object.""" 408 | if not self._data: 409 | self._data = self.container.get_object_by_name( 410 | self.name, 411 | download=True 412 | )._data 413 | return self._data 414 | 415 | @property 416 | def url(self): 417 | """Get the URL of an object.""" 418 | object_name = urllib.quote(self.name) 419 | return '%s/%s' % (self.container.url, object_name) 420 | 421 | def delete(self): 422 | """Delete the object.""" 423 | self.container.delete_object(self) 424 | 425 | def copy(self, to_container=None, new_object_name=None): 426 | """Copy an object from a container to another one. 427 | 428 | Containers must be in the same region. Both containers may be 429 | the same. Content-Type is read from the original object if 430 | available, otherwise it is guessed with file name, defaults to 431 | None if impossible to guess. 432 | 433 | :param to_container: Container where the object will be copied 434 | to. If None copy into the same container. 435 | :param new_object_name: Name of the new object. If None new name 436 | is taken from the original name. 437 | """ 438 | self.container.copy_object(self, to_container, new_object_name) 439 | -------------------------------------------------------------------------------- /runabove/tests/storage.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014, OVH 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # Except as contained in this notice, the name of OVH and or its trademarks 24 | # (and among others RunAbove) shall not be used in advertising or otherwise to 25 | # promote the sale, use or other dealings in this Software without prior 26 | # written authorization from OVH. 27 | 28 | import unittest 29 | import json 30 | import mock 31 | import runabove 32 | 33 | class TestContainerManager(unittest.TestCase): 34 | """Test storage using RunAbove API. 35 | 36 | To play with objects we use swiftclient and we trust it's 37 | tested properly :) 38 | """ 39 | 40 | name = 'test2' 41 | region = 'SBG-1' 42 | answer_list = '''[ 43 | { 44 | "totalObjects": 5, 45 | "name": "test", 46 | "stored": 1024, 47 | "region": "SBG-1" 48 | }, 49 | { 50 | "totalObjects": 0, 51 | "name": "test2", 52 | "stored": 0, 53 | "region": "SBG-1" 54 | } 55 | ]''' 56 | 57 | answer_one = '''{ 58 | "totalObjects": 0, 59 | "name": "test2", 60 | "stored": 0, 61 | "region": "SBG-1" 62 | }''' 63 | 64 | answer_token = '''{ 65 | "token": { 66 | "catalog": [ 67 | { 68 | "endpoints": [ 69 | { 70 | "id": "af64asqa26fda457c0e974f3f", 71 | "interface": "public", 72 | "legacy_endpoint_id": "fa56f4as64c9a8f4asdf496", 73 | "region": "SBG-1", 74 | "url": "https://network.compute.sbg-1.runabove.io/" 75 | }, 76 | { 77 | "id": "5af5d46as48q911zs654fd69fc84", 78 | "interface": "public", 79 | "legacy_endpoint_id": "q984fSDFsa4654164asd98f42c", 80 | "region": "BHS-1", 81 | "url": "https://network.compute.bhs-1.runabove.io/" 82 | } 83 | ], 84 | "id": "022012d24e3c446948qwef6as135c68j7uy97", 85 | "type": "network" 86 | }, 87 | { 88 | "endpoints": [ 89 | { 90 | "id": "asf489a4f541q4f985s1f631a89a7ffd", 91 | "interface": "public", 92 | "legacy_endpoint_id": "f7a1afas65qfsASDc1456qf6", 93 | "region": "BHS-1", 94 | "url": "https://storage.bhs-1.runabove.io/v1/AUTH_fRs614a" 95 | }, 96 | { 97 | "id": "aq98465ASDG46543dfag46eg86eg1s32", 98 | "interface": "public", 99 | "legacy_endpoint_id": "fAFASd73251aplnxzq9899eb68c7", 100 | "region": "SBG-1", 101 | "url": "https://storage.sbg-1.runabove.io/v1/AUTH_4f6sa5df" 102 | } 103 | ], 104 | "id": "3c7237csdfasd45f4615a654dc9awd4f", 105 | "type": "object-store" 106 | } 107 | ], 108 | "expires_at": "2014-07-05T10:40:02.799784Z", 109 | "issued_at": "2014-07-04T10:40:02.799807Z" 110 | }, 111 | "X-Auth-Token": "mbRArjDDI6fpZQRaxg98USPsz1fuK3Jl17ZHxb" 112 | }''' 113 | 114 | answer_token_empty = '''{ 115 | "token": { 116 | "catalog": [], 117 | "expires_at": "2014-07-05T10:40:02.799784Z", 118 | "issued_at": "2014-07-04T10:40:02.799807Z" 119 | }, 120 | "X-Auth-Token": "mbRArjDDI6fpZQRaxg98USPsz1fuK3Jl17ZHxb" 121 | }''' 122 | 123 | @mock.patch('runabove.wrapper_api') 124 | @mock.patch('runabove.client') 125 | def setUp(self, mock_wrapper, mock_client): 126 | self.mock_wrapper = mock_wrapper 127 | self.mock_client = mock_client 128 | self.mock_client.regions = runabove.region.RegionManager(mock_wrapper, 129 | mock_client) 130 | self.containers = runabove.storage.ContainerManager(mock_wrapper, 131 | mock_client) 132 | 133 | def test_base_path(self): 134 | self.assertEquals(self.containers.basepath, '/storage') 135 | 136 | def test_list(self): 137 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 138 | container_list = self.containers.list() 139 | self.mock_wrapper.get.assert_called_once_with(self.containers.basepath) 140 | self.assertIsInstance(container_list, list) 141 | self.assertEquals(len(container_list), 2) 142 | for container in container_list: 143 | self.assertIsInstance(container, runabove.storage.Container) 144 | 145 | def test_list_by_region(self): 146 | self.mock_wrapper.get.return_value = json.loads(self.answer_list) 147 | container_list = self.containers.list_by_region(self.region) 148 | self.mock_wrapper.get.assert_called_once_with( 149 | self.containers.basepath, 150 | {'region': self.region} 151 | ) 152 | self.assertIsInstance(container_list, list) 153 | self.assertEquals(len(container_list), 2) 154 | for container in container_list: 155 | self.assertIsInstance(container, runabove.storage.Container) 156 | self.assertIsInstance(container.region, runabove.region.Region) 157 | self.assertEquals(container.region.name, self.region) 158 | 159 | def test_get_by_name(self): 160 | self.mock_wrapper.get.return_value = json.loads(self.answer_one) 161 | container = self.containers.get_by_name(self.region, self.name) 162 | self.mock_wrapper.get.assert_called_once_with( 163 | self.containers.basepath + '/' + 164 | self.containers._api.encode_for_api(self.name), 165 | {'region': self.region} 166 | ) 167 | self.assertIsInstance(container, runabove.storage.Container) 168 | self.assertEquals(container.name, self.name) 169 | 170 | def test_get_endpoint(self): 171 | self.mock_wrapper.get.return_value = json.loads(self.answer_token) 172 | result = self.containers._get_endpoints() 173 | self.mock_wrapper.get.assert_called_once_with('/token') 174 | self.assertIsInstance(result, tuple) 175 | self.assertEquals(len(result), 2) 176 | for endpoint in result[0]: 177 | self.assertIsInstance(endpoint, dict) 178 | self.assertIsInstance(endpoint['url'], unicode) 179 | self.assertIsInstance(endpoint['region'], unicode) 180 | 181 | def test_get_endpoint_without_object_storage(self): 182 | self.mock_wrapper.get.return_value = json.loads( 183 | self.answer_token_empty 184 | ) 185 | with self.assertRaises(runabove.exception.ResourceNotFoundError): 186 | result = self.containers._get_endpoints() 187 | self.mock_wrapper.get.assert_called_once_with('/token') 188 | 189 | @mock.patch('runabove.storage.ContainerManager._get_endpoints') 190 | @mock.patch('swiftclient.client.Connection') 191 | def test_get_swift_clients(self, mock_get_endpoints, mock_swiftclient): 192 | endpoints = ([{'url': 'http://url', 'region': 'BHS-1'}], 'token') 193 | self.containers._get_endpoints.return_value = endpoints 194 | swifts = self.containers._get_swift_clients() 195 | self.containers._get_endpoints.assert_called_once() 196 | self.assertIsInstance(swifts, dict) 197 | 198 | @mock.patch('swiftclient.client.Connection') 199 | def test_swift_call(self, mock_swiftclient): 200 | swifts = { 201 | 'BHS-1': { 202 | 'client' : mock_swiftclient, 203 | 'endpoint' : 'http://endpoint' 204 | } 205 | } 206 | self.containers._swifts = swifts 207 | self.containers._swift_call('BHS-1', 'put_container') 208 | 209 | @mock.patch('runabove.storage.ContainerManager._swift_call') 210 | def test_delete(self, mock_swift_call): 211 | self.containers.delete(self.region, self.name) 212 | mock_swift_call.assert_called_once_with( 213 | self.region, 214 | 'delete_container', 215 | self.name 216 | ) 217 | 218 | @mock.patch('runabove.storage.ContainerManager.get_by_name') 219 | @mock.patch('runabove.storage.ContainerManager._swift_call') 220 | def test_create_public(self, mock_swift_call, mock_get_by_name): 221 | self.containers.create(self.region, self.name, public=True) 222 | mock_swift_call.assert_called_once_with( 223 | self.region, 224 | 'put_container', 225 | self.name, 226 | headers={'X-Container-Read': '.r:*,.rlistings'} 227 | ) 228 | mock_get_by_name.assert_called_once_with(self.region, self.name) 229 | 230 | @mock.patch('runabove.storage.ContainerManager.get_by_name') 231 | @mock.patch('runabove.storage.ContainerManager._swift_call') 232 | def test_create_private(self, mock_swift_call, mock_get_by_name): 233 | self.containers.create(self.region, self.name) 234 | mock_swift_call.assert_called_once_with( 235 | self.region, 236 | 'put_container', 237 | self.name, 238 | headers={} 239 | ) 240 | mock_get_by_name.assert_called_once_with(self.region, self.name) 241 | 242 | @mock.patch('runabove.storage.ContainerManager._swift_call') 243 | def test_set_public(self, mock_swift_call): 244 | self.containers.set_public(self.region, self.name) 245 | mock_swift_call.assert_called_once_with( 246 | self.region, 247 | 'post_container', 248 | self.name, 249 | headers = {'X-Container-Read': '.r:*,.rlistings'} 250 | ) 251 | 252 | @mock.patch('runabove.storage.ContainerManager._swift_call') 253 | def test_set_public_with_private(self, mock_swift_call): 254 | self.containers.set_public(self.region, self.name, public=False) 255 | mock_swift_call.assert_called_once_with( 256 | self.region, 257 | 'post_container', 258 | self.name, 259 | headers = {'X-Container-Read': ''} 260 | ) 261 | 262 | @mock.patch('runabove.storage.ContainerManager.set_public') 263 | def test_set_private(self, mock_set_public): 264 | self.containers.set_private(self.region, self.name) 265 | mock_set_public.assert_called_once_with( 266 | self.region, 267 | self.name, 268 | public=False 269 | ) 270 | 271 | def test_get_region_url(self): 272 | swifts = { 273 | 'BHS-1': { 274 | 'endpoint' : 'http://endpoint' 275 | } 276 | } 277 | self.containers._swifts = swifts 278 | url = self.containers.get_region_url('BHS-1') 279 | self.assertEquals(url, 'http://endpoint') 280 | 281 | def test_get_region_url_not_found(self): 282 | self.containers._swifts = {} 283 | with self.assertRaises(runabove.exception.ResourceNotFoundError): 284 | self.containers.get_region_url('BHS-1') 285 | 286 | @mock.patch('runabove.storage.ContainerManager._swift_call') 287 | def test_copy_object(self, mock_swift_call): 288 | self.containers.copy_object(self.region, self.name, 'Test') 289 | mock_swift_call.assert_called_once_with( 290 | self.region, 291 | 'put_object', 292 | self.name, 293 | 'Test', 294 | None, 295 | headers = {'X-Copy-From': '/' + self.name + '/Test'}, 296 | content_length=0, 297 | content_type=None 298 | ) 299 | 300 | @mock.patch('runabove.storage.ContainerManager._swift_call') 301 | def test_copy_object_other_container(self, mock_swift_call): 302 | self.containers.copy_object(self.region, self.name, 'Test', 303 | to_container='test1') 304 | mock_swift_call.assert_called_once_with( 305 | self.region, 306 | 'put_object', 307 | 'test1', 308 | 'Test', 309 | None, 310 | headers = {'X-Copy-From': '/' + self.name + '/Test'}, 311 | content_length=0, 312 | content_type=None 313 | ) 314 | 315 | @mock.patch('runabove.storage.ContainerManager._get_swift_clients') 316 | def test_swifts(self, mock_get_swift_clients): 317 | mock_get_swift_clients.return_value = {} 318 | swifts = self.containers.swifts 319 | mock_get_swift_clients.assert_called_once() 320 | self.assertIsInstance(swifts, dict) 321 | 322 | 323 | class TestContainer(unittest.TestCase): 324 | 325 | container_name = 'MyTestContainer' 326 | 327 | answer_list = '''[ 328 | [""], 329 | [ 330 | { 331 | "name": "obj1", 332 | "bytes": 20, 333 | "last_modified": "Thu, 31 Jul 2014 07:57:30 GMT", 334 | "content_type": "image/png" 335 | }, 336 | { 337 | "name": "obj2", 338 | "bytes": 26, 339 | "last_modified": "Thu, 31 Jul 2014 07:58:30 GMT", 340 | "content_type": "image/png" 341 | } 342 | ] 343 | ]''' 344 | 345 | answer_head_object = { 346 | 'content-length': '0', 347 | 'accept-ranges': 'bytes', 348 | 'last-modified': 'Thu, 31 Jul 2014 07:57:30 GMT', 349 | 'connection': 'close', 350 | 'etag': 'd41d8cd99f00b204e9800998ecf8427f', 351 | 'x-timestamp': '1406793450.95376', 352 | 'x-trans-id': 'txbcbed42b0efd46a7aace3-0054da0217', 353 | 'date': 'Thu, 31 Jul 2014 08:45:11 GMT', 354 | 'content-type': 'application/octet-stream' 355 | } 356 | 357 | answer_get_object = ( 358 | { 359 | 'content-length': '0', 360 | 'accept-ranges': 'bytes', 361 | 'last-modified': 'Thu, 31 Jul 2014 07:57:30 GMT', 362 | 'connection': 'close', 363 | 'etag': 'd41d8cd99f00b204e9800998ecf8427f', 364 | 'x-timestamp': '1406793450.95376', 365 | 'x-trans-id': 'txbcbed42b0efd46a7aace3-0054da0217', 366 | 'date': 'Thu, 31 Jul 2014 08:45:11 GMT', 367 | 'content-type': 'application/octet-stream' 368 | }, 369 | 'data' 370 | ) 371 | 372 | @mock.patch('runabove.region.Region') 373 | @mock.patch('runabove.storage.ContainerManager') 374 | def setUp(self, mock_containers, mock_region): 375 | self.mock_containers = mock_containers 376 | self.mock_region = mock_region 377 | self.container = runabove.storage.Container( 378 | self.mock_containers, 379 | self.container_name, 380 | 1024, 381 | 5, 382 | self.mock_region 383 | ) 384 | 385 | def test_list_objects(self): 386 | answer = json.loads(self.answer_list) 387 | self.mock_containers._swift_call.return_value = answer 388 | object_list = self.container.list_objects() 389 | self.mock_containers._swift_call.assert_called_once_with( 390 | self.mock_region.name, 391 | 'get_container', 392 | self.container_name, 393 | full_listing=True 394 | ) 395 | self.assertIsInstance(object_list, list) 396 | self.assertEquals(len(object_list), 2) 397 | for obj in object_list: 398 | self.assertIsInstance(obj, runabove.storage.ObjectStored) 399 | 400 | def _get_object_by_name(self, download=False): 401 | swift_answer = self.answer_head_object 402 | call = 'head_object' 403 | if download: 404 | swift_answer = self.answer_get_object 405 | call = 'get_object' 406 | self.mock_containers._swift_call.return_value = swift_answer 407 | obj = self.container.get_object_by_name('TestObj', download) 408 | self.mock_containers._swift_call.assert_called_once_with( 409 | self.mock_region.name, 410 | call, 411 | self.container_name, 412 | 'TestObj' 413 | ) 414 | self.assertIsInstance(obj, runabove.storage.ObjectStored) 415 | if download: 416 | self.assertEquals(obj._data, 'data') 417 | else: 418 | self.assertEquals(obj._data, None) 419 | 420 | def test_get_object_by_name_without_download(self): 421 | self._get_object_by_name() 422 | 423 | def test_get_object_by_name_with_download(self): 424 | self._get_object_by_name(download=True) 425 | 426 | def test_delete(self): 427 | self.container.delete() 428 | self.mock_containers.delete.assert_called_once_with( 429 | self.mock_region, 430 | self.container 431 | ) 432 | 433 | def test_delete_object(self): 434 | self.container.delete_object('Test') 435 | self.mock_containers._swift_call.assert_called_once_with( 436 | self.mock_region, 437 | 'delete_object', 438 | self.container.name, 439 | 'Test' 440 | ) 441 | 442 | @mock.patch('mimetypes.guess_type') 443 | @mock.patch('runabove.storage.Container.get_object_by_name') 444 | def test_create_object(self, mock_get_object_by_name, mock_guess_type): 445 | mock_guess_type.return_value = ('application/json',) 446 | obj = self.container.create_object('Test', 'content') 447 | self.mock_containers._swift_call.assert_called_once_with( 448 | self.mock_region.name, 449 | 'put_object', 450 | self.container.name, 451 | 'Test', 452 | 'content', 453 | content_type='application/json' 454 | ) 455 | 456 | @mock.patch('runabove.storage.ObjectStored') 457 | def test_copy(self, mock_obj): 458 | to_container = 'CopyTo' 459 | new_object_name = 'NewName' 460 | self.container.copy_object(mock_obj, to_container, new_object_name) 461 | self.mock_containers.copy_object.assert_called_once_with( 462 | self.mock_region.name, 463 | self.container, 464 | mock_obj, 465 | to_container, 466 | new_object_name 467 | ) 468 | 469 | def test_is_public(self): 470 | result = self.container.is_public 471 | self.mock_containers.get_by_name.assert_called_once_with( 472 | self.mock_region.name, 473 | self.container.name 474 | ) 475 | 476 | def test_set_public(self): 477 | self.container.set_public() 478 | self.mock_containers.set_public.assert_called_once_with( 479 | self.mock_region.name, 480 | self.container 481 | ) 482 | 483 | def test_set_private(self): 484 | self.container.set_private() 485 | self.mock_containers.set_private.assert_called_once_with( 486 | self.mock_region.name, 487 | self.container 488 | ) 489 | 490 | def test_url(self): 491 | base_url = 'https://url-of-endpoint' 492 | self.mock_containers.get_region_url.return_value = base_url 493 | url = self.container.url 494 | self.mock_containers.get_region_url.assert_called_once_with( 495 | self.mock_region.name 496 | ) 497 | self.assertEquals(url, base_url + '/' + self.container_name) 498 | 499 | 500 | class TestObjectStored(unittest.TestCase): 501 | 502 | obj_name = 'MyTestObject' 503 | 504 | @mock.patch('runabove.storage.Container') 505 | def setUp(self, mock_container): 506 | self.mock_container = mock_container 507 | self.obj = runabove.storage.ObjectStored( 508 | self.mock_container, 509 | self.obj_name, 510 | 1024, 511 | 'Thu, 31 Jul 2014 07:57:30 GMT', 512 | 'image/png' 513 | ) 514 | 515 | @mock.patch('runabove.storage.ObjectStored') 516 | def test_data(self, mock_obj): 517 | fake_data = 'SomeData' 518 | mock_obj._data = fake_data 519 | self.mock_container.get_object_by_name.return_value = mock_obj 520 | data = self.obj.data 521 | self.mock_container.get_object_by_name.assert_called_once_with( 522 | self.obj.name, 523 | download=True 524 | ) 525 | self.assertEquals(data, fake_data) 526 | 527 | @mock.patch('runabove.storage.ObjectStored') 528 | def test_data_already_downloaded(self, mock_obj): 529 | fake_data = 'SomeData' 530 | self.obj._data = fake_data 531 | data = self.obj.data 532 | self.mock_container.get_object_by_name.assert_not_called() 533 | self.assertEquals(data, fake_data) 534 | 535 | def test_url(self): 536 | base_url = 'https://url-of-endpoint/containerName' 537 | self.mock_container.url = base_url 538 | url = self.obj.url 539 | self.assertEquals(url, base_url + '/' + self.obj_name) 540 | 541 | def test_delete(self): 542 | self.obj.delete() 543 | self.mock_container.delete_object.assert_called_once_with( 544 | self.obj 545 | ) 546 | 547 | def test_copy(self): 548 | to_container = 'CopyTo' 549 | new_object_name = 'NewName' 550 | self.obj.copy(to_container, new_object_name) 551 | self.mock_container.copy_object.assert_called_once_with( 552 | self.obj, 553 | to_container, 554 | new_object_name 555 | ) 556 | 557 | if __name__ == '__main__': 558 | unittest.main() 559 | -------------------------------------------------------------------------------- /examples/facecat/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); --------------------------------------------------------------------------------