diff --git libcloud/compute/drivers/vultr.py libcloud/compute/drivers/vultr.py new file mode 100644 index 0000000..22db689 --- /dev/null +++ libcloud/compute/drivers/vultr.py @@ -0,0 +1,181 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Vultr Driver +""" + +from libcloud.utils.py3 import httplib + +from libcloud.common.base import ConnectionKey, JsonResponse +from libcloud.compute.types import Provider, NodeState, InvalidCredsError +from libcloud.compute.base import NodeDriver +from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation +import urllib +import time + + +class VultrResponse(JsonResponse): + def parse_error(self): + if self.status == httplib.FORBIDDEN: + # Hacky, but Vultr error responses are awful + raise InvalidCredsError(self.body) + else: + body = self.parse_body() + return body + + +class VultrConnection(ConnectionKey): + """ + Connection class for the Vultr driver. + """ + + host = 'api.vultr.com' + responseCls = VultrResponse + + def add_default_params(self, params): + """ + Add parameters that are necessary for every request + + This method add ``api_key`` to + the request. + """ + params['api_key'] = self.key + return params + + def encode_data(self, data): + return urllib.urlencode(data) + + def get(self, url): + return self.request(url) + + def post(self, url, data): + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + return self.request(url, data=data, headers=headers, method='POST') + + +class VultrNodeDriver(NodeDriver): + """ + VultrNode node driver. + """ + + connectionCls = VultrConnection + + type = Provider.VULTR + name = 'Vultr' + website = 'https://www.vultr.com' + + NODE_STATE_MAP = {'pending': NodeState.PENDING, + 'active': NodeState.RUNNING} + + def list_nodes(self): + return self.get('/v1/server/list', self._to_node) + + def list_locations(self): + return self.get('/v1/regions/list', self._to_location) + + def list_sizes(self): + return self.get('/v1/plans/list', self._to_size) + + def list_images(self): + return self.get('/v1/os/list', self._to_image) + + def create_node(self, name, size, image, location, **kwargs): + params = {'DCID': location.id, 'VPSPLANID': size.id, + 'OSID': image.id, 'label': name} + + result = self.connection.post('/v1/server/create', params) + if result.status != httplib.OK: + return False + + subid = result.object['SUBID'] + + polling = True + created_node = None + while polling: + nodes = self.list_nodes() + for node in nodes: + if node.id == subid: + polling = False + created_node = node + break + + if polling is True: + time.sleep(1) + + return created_node + + def reboot_node(self, node): + params = {'SUBID': node.id} + res = self.connection.post('/v1/server/reboot', params) + + return res.status == httplib.OK + + def destroy_node(self, node): + params = {'SUBID': node.id} + res = self.connection.post('/v1/server/destroy', params) + + return res.status == httplib.OK + + def get(self, url, fn): + data = self.connection.get(url).object + sorted_key = sorted(data) + return [fn(data[key]) for key in sorted_key] + + def _to_node(self, data): + if 'status' in data: + state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN) + if state == NodeState.RUNNING and \ + data['power_status'] != 'running': + state = NodeState.STOPPED + else: + state = NodeState.UNKNOWN + + if 'main_ip' in data and data['main_ip'] is not None: + public_ips = [data['main_ip']] + else: + public_ips = [] + + extra_keys = [] + extra = {} + for key in extra_keys: + if key in data: + extra[key] = data[key] + + node = Node(id=data['SUBID'], name=data['label'], state=state, + public_ips=public_ips, private_ips=None, extra=extra, + driver=self) + + return node + + def _to_location(self, data): + return NodeLocation(id=data['DCID'], name=data['name'], + country=data['country'], driver=self) + + def _to_size(self, data): + extra = {'vcpu_count': int(data['vcpu_count'])} + ram = int(data['ram']) + disk = int(data['disk']) + bandwidth = float(data['bandwidth']) + price = float(data['price_per_month']) + + return NodeSize(id=data['VPSPLANID'], name=data['name'], + ram=ram, disk=disk, + bandwidth=bandwidth, price=price, + extra=extra, driver=self) + + def _to_image(self, data): + extra = {'arch': data['arch'], 'family': data['family']} + return NodeImage(id=data['OSID'], name=data['name'], extra=extra, + driver=self) diff --git libcloud/compute/providers.py libcloud/compute/providers.py index b449e78..bdceafc 100644 --- libcloud/compute/providers.py +++ libcloud/compute/providers.py @@ -151,6 +151,8 @@ DRIVERS = { ('libcloud.compute.drivers.vsphere', 'VSphereNodeDriver'), Provider.PROFIT_BRICKS: ('libcloud.compute.drivers.profitbricks', 'ProfitBricksNodeDriver'), + Provider.VULTR: + ('libcloud.compute.drivers.vultr', 'VultrNodeDriver'), # Deprecated Provider.CLOUDSIGMA_US: diff --git libcloud/compute/types.py libcloud/compute/types.py index f07bc48..c21a4d1 100644 --- libcloud/compute/types.py +++ libcloud/compute/types.py @@ -77,6 +77,7 @@ class Provider(object): :cvar OUTSCALE_SAS: Outscale SAS driver. :cvar OUTSCALE_INC: Outscale INC driver. :cvar PROFIT_BRICKS: ProfitBricks driver. + :cvar VULTR: vultr driver. """ DUMMY = 'dummy' EC2 = 'ec2_us_east' @@ -124,6 +125,7 @@ class Provider(object): OUTSCALE_INC = 'outscale_inc' VSPHERE = 'vsphere' PROFIT_BRICKS = 'profitbricks' + VULTR = 'vultr' # OpenStack based providers HPCLOUD = 'hpcloud' diff --git libcloud/test/compute/fixtures/vultr/list_images.json libcloud/test/compute/fixtures/vultr/list_images.json new file mode 100644 index 0000000..139f2af --- /dev/null +++ libcloud/test/compute/fixtures/vultr/list_images.json @@ -0,0 +1 @@ +{"127":{"OSID":127,"name":"CentOS 6 x64","arch":"x64","family":"centos","windows":false},"147":{"OSID":147,"name":"CentOS 6 i386","arch":"i386","family":"centos","windows":false},"162":{"OSID":162,"name":"CentOS 5 x64","arch":"x64","family":"centos","windows":false},"163":{"OSID":163,"name":"CentOS 5 i386","arch":"i386","family":"centos","windows":false},"167":{"OSID":167,"name":"CentOS 7 x64","arch":"x64","family":"centos","windows":false},"160":{"OSID":160,"name":"Ubuntu 14.04 x64","arch":"x64","family":"ubuntu","windows":false},"161":{"OSID":161,"name":"Ubuntu 14.04 i386","arch":"i386","family":"ubuntu","windows":false},"128":{"OSID":128,"name":"Ubuntu 12.04 x64","arch":"x64","family":"ubuntu","windows":false},"148":{"OSID":148,"name":"Ubuntu 12.04 i386","arch":"i386","family":"ubuntu","windows":false},"181":{"OSID":181,"name":"Ubuntu 14.10 x64","arch":"x64","family":"ubuntu","windows":false},"182":{"OSID":182,"name":"Ubuntu 14.10 i386","arch":"i386","family":"ubuntu","windows":false},"139":{"OSID":139,"name":"Debian 7 x64 (wheezy)","arch":"x64","family":"debian","windows":false},"152":{"OSID":152,"name":"Debian 7 i386 (wheezy)","arch":"i386","family":"debian","windows":false},"140":{"OSID":140,"name":"FreeBSD 10 x64","arch":"x64","family":"freebsd","windows":false},"124":{"OSID":124,"name":"Windows 2012 R2 x64","arch":"x64","family":"windows","windows":true},"159":{"OSID":159,"name":"Custom","arch":"x64","family":"iso","windows":false},"164":{"OSID":164,"name":"Snapshot","arch":"x64","family":"snapshot","windows":false},"180":{"OSID":180,"name":"Backup","arch":"x64","family":"backup","windows":false}} \ No newline at end of file diff --git libcloud/test/compute/fixtures/vultr/list_locations.json libcloud/test/compute/fixtures/vultr/list_locations.json new file mode 100644 index 0000000..6cf6d0b --- /dev/null +++ libcloud/test/compute/fixtures/vultr/list_locations.json @@ -0,0 +1 @@ +{"6":{"DCID":"6","name":"Atlanta","country":"US","continent":"North America","state":"GA"},"2":{"DCID":"2","name":"Chicago","country":"US","continent":"North America","state":"IL"},"3":{"DCID":"3","name":"Dallas","country":"US","continent":"North America","state":"TX"},"5":{"DCID":"5","name":"Los Angeles","country":"US","continent":"North America","state":"CA"},"39":{"DCID":"39","name":"Miami","country":"US","continent":"","state":"FL"},"1":{"DCID":"1","name":"New Jersey","country":"US","continent":"North America","state":"NJ"},"4":{"DCID":"4","name":"Seattle","country":"US","continent":"North America","state":"WA"},"12":{"DCID":"12","name":"Silicon Valley","country":"US","continent":"North America","state":"CA"},"7":{"DCID":"7","name":"Amsterdam","country":"NL","continent":"Europe","state":""},"25":{"DCID":"25","name":"Tokyo","country":"JP","continent":"Asia","state":""},"8":{"DCID":"8","name":"London","country":"GB","continent":"Europe","state":""},"24":{"DCID":"24","name":"France","country":"FR","continent":"Europe","state":""},"9":{"DCID":"9","name":"Frankfurt","country":"DE","continent":"Europe","state":""},"19":{"DCID":"19","name":"Australia","country":"AU","continent":"Australia","state":""}} diff --git libcloud/test/compute/fixtures/vultr/list_nodes.json libcloud/test/compute/fixtures/vultr/list_nodes.json new file mode 100644 index 0000000..e12ba4a --- /dev/null +++ libcloud/test/compute/fixtures/vultr/list_nodes.json @@ -0,0 +1 @@ +{"1":{"SUBID":"1","os":"Ubuntu 12.04 x64","ram":"1024 MB","disk":"Virtual 20 GB","main_ip":"108.61.206.153","vcpu_count":"1","location":"Los Angeles","DCID":"5","default_password":"twizewnatpom!7","date_created":"2014-03-21 12:46:35","pending_charges":"1.92","status":"active","cost_per_month":"7.00","current_bandwidth_gb":0.929,"allowed_bandwidth_gb":"2000","netmask_v4":"255.255.254.0","gateway_v4":"108.61.206.1","power_status":"running","VPSPLANID":"30","v6_network":"::","v6_main_ip":"","v6_network_size":"0","label":"","internal_ip":"","kvm_url":"https:\/\/my.vultr.com\/subs\/vps\/novnc\/api.php?data=IF3C6VCEN5NFOZ3VMM3FOV3JJVXUQV3OHBWDG6TUKI3VST3JMFDXOOCMIE3HCTBWKVJXOZZYF5BVMZ3IM5XXGWRZIVZW4S2WKAVTSMTQHFCG4QTCNFUEKOCXKY3CW3LGNF4HIVTVJ5GXM4CJORTU6SKYOBDE6WJVMZ3E4ZSVOB2FQ4KYF5DXC5CTJI4FETDIGBITQQZPGFLXKTSRKRJS6ODOMFKDSNLLNVETONSNKA2XQ6CWLJMW6L2EGI2U6SDNN5FGUTJYNB3UC5DXN46Q","auto_backups":"no"},"2":{"SUBID":"2","os":"Ubuntu 14.04 x64","ram":"768 MB","disk":"Virtual 15 GB","main_ip":"104.207.153.143","vcpu_count":"1","location":"Los Angeles","DCID":"5","default_password":"cewxoaezap!0","date_created":"2014-11-08 14:12:13","pending_charges":"0.01","status":"active","cost_per_month":"5.00","current_bandwidth_gb":0,"allowed_bandwidth_gb":"1000","netmask_v4":"255.255.254.0","gateway_v4":"104.207.152.1","power_status":"running","VPSPLANID":"29","v6_network":"::","v6_main_ip":"","v6_network_size":"0","label":"vultr-test1","internal_ip":"","kvm_url":"https:\/\/my.vultr.com\/subs\/vps\/novnc\/api.php?data=NBUUYMTDI4VXGVZXOFBE6UBWKFLWE43MI5EFOR3MJNZW4NRXLFBHA33BHF3C63LSOJRXAU2PO5GHM5LPOFAW2MSDMFZWUMCNNJRG6TRWJREWYNBLG5VTG2DEGIYWITKIGV2FA3JTNJVEETLOGBHFG42XLEZHG22VFNWHE5RUKFIWU3DSOJCS6WDQGJRDIZRPIU2HILZTKB4E4MZSNZIFEQ3SOFSDANCBHBBEWRLVGZEUEVDSJVQVKOKZNQ4GKSRSIJEG62TWMREG6USNIE6Q","auto_backups":"no"}} diff --git libcloud/test/compute/fixtures/vultr/list_sizes.json libcloud/test/compute/fixtures/vultr/list_sizes.json new file mode 100644 index 0000000..ded841d --- /dev/null +++ libcloud/test/compute/fixtures/vultr/list_sizes.json @@ -0,0 +1 @@ +{"31":{"VPSPLANID":"31","name":"768 MB RAM,15 GB SSD,0.20 TB BW","vcpu_count":"1","ram":"768","disk":"15","bandwidth":"0.20","bandwidth_gb":"204.8","price_per_month":"5.00","windows":false},"29":{"VPSPLANID":"29","name":"768 MB RAM,15 GB SSD,1.00 TB BW","vcpu_count":"1","ram":"768","disk":"15","bandwidth":"1.00","bandwidth_gb":"1024","price_per_month":"5.00","windows":false},"32":{"VPSPLANID":"32","name":"1024 MB RAM,20 GB SSD,0.40 TB BW","vcpu_count":"1","ram":"1024","disk":"20","bandwidth":"0.40","bandwidth_gb":"409.6","price_per_month":"7.00","windows":false},"30":{"VPSPLANID":"30","name":"1024 MB RAM,20 GB SSD,2.00 TB BW","vcpu_count":"1","ram":"1024","disk":"20","bandwidth":"2.00","bandwidth_gb":"2048","price_per_month":"7.00","windows":false},"3":{"VPSPLANID":"3","name":"2048 MB RAM,40 GB SSD,3.00 TB BW","vcpu_count":"2","ram":"2048","disk":"40","bandwidth":"3.00","bandwidth_gb":"3072","price_per_month":"15.00","windows":false},"8":{"VPSPLANID":"8","name":"2048 MB RAM,40 GB SSD,0.60 TB BW","vcpu_count":"2","ram":"2048","disk":"40","bandwidth":"0.60","bandwidth_gb":"614.4","price_per_month":"15.00","windows":false},"33":{"VPSPLANID":"33","name":"4096 MB RAM,65 GB SSD,0.80 TB BW","vcpu_count":"2","ram":"4096","disk":"65","bandwidth":"0.80","bandwidth_gb":"819.2","price_per_month":"35.00","windows":false},"27":{"VPSPLANID":"27","name":"4096 MB RAM,65 GB SSD,4.00 TB BW","vcpu_count":"2","ram":"4096","disk":"65","bandwidth":"4.00","bandwidth_gb":"4096","price_per_month":"35.00","windows":false},"28":{"VPSPLANID":"28","name":"8192 MB RAM,120 GB SSD,5.00 TB BW","vcpu_count":"4","ram":"8192","disk":"120","bandwidth":"5.00","bandwidth_gb":"5120","price_per_month":"70.00","windows":false},"34":{"VPSPLANID":"34","name":"8192 MB RAM,120 GB SSD,1.00 TB BW","vcpu_count":"4","ram":"8192","disk":"120","bandwidth":"1.00","bandwidth_gb":"1024","price_per_month":"70.00","windows":false},"11":{"VPSPLANID":"11","name":"512 MB RAM,160 GB SATA,1.00 TB BW","vcpu_count":"1","ram":"512","disk":"160","bandwidth":"1.00","bandwidth_gb":"1024","price_per_month":"5.00","windows":false},"78":{"VPSPLANID":"78","name":"16384 MB RAM,250 GB SSD,6.00 TB BW","vcpu_count":"8","ram":"16384","disk":"250","bandwidth":"6.00","bandwidth_gb":"6144","price_per_month":"149.95","windows":false},"71":{"VPSPLANID":"71","name":"16384 MB RAM,250 GB SSD,8.00 TB BW","vcpu_count":"4","ram":"16384","disk":"250","bandwidth":"8.00","bandwidth_gb":"8192","price_per_month":"125.00","windows":false},"68":{"VPSPLANID":"68","name":"16384 MB RAM,250 GB SSD,1.60 TB BW","vcpu_count":"4","ram":"16384","disk":"250","bandwidth":"1.60","bandwidth_gb":"1638.4","price_per_month":"125.00","windows":false},"12":{"VPSPLANID":"12","name":"1024 MB RAM,320 GB SATA,2.00 TB BW","vcpu_count":"1","ram":"1024","disk":"320","bandwidth":"2.00","bandwidth_gb":"2048","price_per_month":"8.00","windows":false},"62":{"VPSPLANID":"62","name":"1024 MB RAM,320 GB SATAPERF,1.00 TB BW, 10GigE","vcpu_count":"1","ram":"1024","disk":"320","bandwidth":"1.00","bandwidth_gb":"1024","price_per_month":"15.00","windows":false},"79":{"VPSPLANID":"79","name":"32768 MB RAM,400 GB SSD,7.00 TB BW","vcpu_count":"12","ram":"32768","disk":"400","bandwidth":"7.00","bandwidth_gb":"7168","price_per_month":"299.95","windows":false},"80":{"VPSPLANID":"80","name":"49152 MB RAM,600 GB SSD,8.00 TB BW","vcpu_count":"16","ram":"49152","disk":"600","bandwidth":"8.00","bandwidth_gb":"8192","price_per_month":"429.95","windows":false},"13":{"VPSPLANID":"13","name":"2048 MB RAM,640 GB SATA,3.00 TB BW","vcpu_count":"1","ram":"2048","disk":"640","bandwidth":"3.00","bandwidth_gb":"3072","price_per_month":"15.00","windows":false},"63":{"VPSPLANID":"63","name":"2048 MB RAM,640 GB SATAPERF,2.00 TB BW, 10GigE","vcpu_count":"1","ram":"2048","disk":"640","bandwidth":"2.00","bandwidth_gb":"2048","price_per_month":"25.00","windows":false},"81":{"VPSPLANID":"81","name":"65536 MB RAM,800 GB SSD,9.00 TB BW","vcpu_count":"24","ram":"65536","disk":"800","bandwidth":"9.00","bandwidth_gb":"9216","price_per_month":"599.95","windows":false},"64":{"VPSPLANID":"64","name":"4096 MB RAM,1280 GB SATAPERF,3.00 TB BW, 10GigE","vcpu_count":"2","ram":"4096","disk":"1280","bandwidth":"3.00","bandwidth_gb":"3072","price_per_month":"50.00","windows":false}} \ No newline at end of file diff --git libcloud/test/compute/test_vultr.py libcloud/test/compute/test_vultr.py new file mode 100644 index 0000000..066cc86 --- /dev/null +++ libcloud/test/compute/test_vultr.py @@ -0,0 +1,114 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import unittest + +try: + import simplejson as json +except ImportError: + import json # NOQA + +from libcloud.utils.py3 import httplib + +from libcloud.compute.drivers.vultr import VultrNodeDriver + +from libcloud.test import LibcloudTestCase, MockHttpTestCase +from libcloud.test.file_fixtures import ComputeFileFixtures +from libcloud.test.secrets import VULTR_PARAMS + + +# class VultrTests(unittest.TestCase, TestCaseMixin): +class VultrTests(LibcloudTestCase): + + def setUp(self): + VultrNodeDriver.connectionCls.conn_classes = \ + (VultrMockHttp, VultrMockHttp) + VultrMockHttp.type = None + self.driver = VultrNodeDriver(*VULTR_PARAMS) + + def test_list_images_success(self): + images = self.driver.list_images() + self.assertTrue(len(images) >= 1) + + image = images[0] + self.assertTrue(image.id is not None) + self.assertTrue(image.name is not None) + + def test_list_sizes_success(self): + sizes = self.driver.list_sizes() + self.assertTrue(len(sizes) == 22) + + size = sizes[0] + self.assertTrue(size.id is not None) + self.assertEqual(size.name, '512 MB RAM,160 GB SATA,1.00 TB BW') + self.assertEqual(size.ram, 512) + + size = sizes[21] + self.assertTrue(size.id is not None) + self.assertEqual(size.name, '65536 MB RAM,800 GB SSD,9.00 TB BW') + self.assertEqual(size.ram, 65536) + + def test_list_locations_success(self): + locations = self.driver.list_locations() + self.assertTrue(len(locations) >= 1) + + location = locations[0] + self.assertEqual(location.id, '1') + self.assertEqual(location.name, 'New Jersey') + + def test_list_nodes_success(self): + nodes = self.driver.list_nodes() + self.assertEqual(len(nodes), 2) + self.assertEqual(nodes[0].id, '1') + self.assertEqual(nodes[0].public_ips, ['108.61.206.153']) + + def test_reboot_node_success(self): + node = self.driver.list_nodes()[0] + result = self.driver.reboot_node(node) + self.assertTrue(result) + + def test_destroy_node_success(self): + node = self.driver.list_nodes()[0] + result = self.driver.destroy_node(node) + self.assertTrue(result) + + +class VultrMockHttp(MockHttpTestCase): + fixtures = ComputeFileFixtures('vultr') + + def _v1_regions_list(self, method, url, body, headers): + body = self.fixtures.load('list_locations.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_os_list(self, method, url, body, headers): + body = self.fixtures.load('list_images.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_plans_list(self, method, url, body, headers): + body = self.fixtures.load('list_sizes.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_server_list(self, method, url, body, headers): + body = self.fixtures.load('list_nodes.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_server_destroy(self, method, url, body, headers): + return (httplib.OK, "", {}, httplib.responses[httplib.OK]) + + def _v1_server_reboot(self, method, url, body, headers): + return (httplib.OK, "", {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git libcloud/test/secrets.py-dist libcloud/test/secrets.py-dist index 940f455..3bac5ae 100644 --- libcloud/test/secrets.py-dist +++ libcloud/test/secrets.py-dist @@ -45,6 +45,7 @@ HOSTVIRTUAL_PARAMS = ('key',) DIGITAL_OCEAN_PARAMS = ('user', 'key') CLOUDFRAMES_PARAMS = ('key', 'secret', False, 'host', 8888) PROFIT_BRICKS_PARAMS = ('user', 'key') +VULTR_PARAMS = ('key') # Storage STORAGE_S3_PARAMS = ('key', 'secret')