├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── linode ├── VEpycurl.py ├── __init__.py ├── api.py ├── deploy_abunch.py ├── fields.py ├── methodcheck.py ├── oop.py ├── shell.py └── tests.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.sw? 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | A Python library to perform low-level Linode API functions. 2 | 3 | Copyright (c) 2010 Timothy J Fontaine 4 | Copyright (c) 2010 Josh Wright 5 | Copyright (c) 2010 Ryan Tucker 6 | Copyright (c) 2008 James C Sinclair 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linode Python Bindings 2 | 3 | 4 | The bindings consist of three pieces: 5 | - api.py: Core library that manages authentication and api calls 6 | - shell.py: A command line interface to api.py that allows you to invoke 7 | a specific api command quickly 8 | - oop.py: An object oriented interface to api.py inspired by django 9 | 10 | For definitive documentation on how the api works please visit: 11 | https://www.linode.com/api 12 | 13 | ## API Keys 14 | 15 | 16 | When creating an api object you may specify the key manually, or use the 17 | Api.user_getapikey which will return your apikey as well as set the internal 18 | key that will be used for subsequent api calls. 19 | 20 | Both the shell.py and oop.py have mechanisms to pull the api key from the 21 | environment variable LINODE_API_KEY as well. 22 | 23 | ## Batching 24 | 25 | 26 | Batching should be used with care, once enabled all api calls are cached until 27 | Api.batchFlush() is called, however you must remember the order in which calls 28 | were made as that's the order of the list returned to you 29 | 30 | ## License 31 | 32 | 33 | This code is provided under an MIT-style license. Please refer to the LICENSE 34 | file in the root of the project for specifics. 35 | -------------------------------------------------------------------------------- /linode/VEpycurl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2009, Kenneth East 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | """ 25 | 26 | # 27 | # module for VEpycurl - the Very Easy interface to pycurl 28 | # 29 | 30 | from StringIO import StringIO 31 | import urllib 32 | import pycurl 33 | import sys 34 | import os 35 | 36 | class VEpycurl() : 37 | """ 38 | A VERY EASY interface to pycurl, v1.0 39 | Tested on 22Feb09 with python 2.5.1, py25-curl 7.19.0, libcurl/7.19.2, OS-X 10.5.6 40 | """ 41 | 42 | def __init__(self, 43 | userAgent = 'Mozilla/4.0 (compatible; MSIE 8.0)', 44 | followLocation = 1, # follow redirects? 45 | autoReferer = 1, # allow 'referer' to be set normally? 46 | verifySSL = 0, # tell SSL to verify IDs? 47 | useCookies = True, # will hold all pycurl cookies 48 | useSOCKS = False, # use SOCKS5 proxy? 49 | proxy = 'localhost', # SOCKS host 50 | proxyPort = 8080, # SOCKS port 51 | proxyType = 5, # SOCKS protocol 52 | verbose = False, 53 | debug = False, 54 | ) : 55 | self.followLocation = followLocation 56 | self.autoReferer = autoReferer 57 | self.verifySSL = verifySSL 58 | self.useCookies = useCookies 59 | self.useSOCKS = useSOCKS 60 | self.proxy = proxy 61 | self.proxyPort = proxyPort 62 | self.proxyType = proxyType 63 | self.pco = pycurl.Curl() 64 | self.pco.setopt(pycurl.USERAGENT, userAgent) 65 | self.pco.setopt(pycurl.FOLLOWLOCATION, followLocation) 66 | self.pco.setopt(pycurl.MAXREDIRS, 20) 67 | self.pco.setopt(pycurl.CONNECTTIMEOUT, 30) 68 | self.pco.setopt(pycurl.AUTOREFERER, autoReferer) 69 | # SSL verification (True/False) 70 | self.pco.setopt(pycurl.SSL_VERIFYPEER, verifySSL) 71 | self.pco.setopt(pycurl.SSL_VERIFYHOST, verifySSL) 72 | if useCookies == True : 73 | cjf = os.tmpfile() # potential security risk here; see python documentation 74 | self.pco.setopt(pycurl.COOKIEFILE, cjf.name) 75 | self.pco.setopt(pycurl.COOKIEJAR, cjf.name) 76 | if useSOCKS : 77 | # if you wish to use SOCKS, it is configured through these parms 78 | self.pco.setopt(pycurl.PROXY, proxy) 79 | self.pco.setopt(pycurl.PROXYPORT, proxyPort) 80 | self.pco.setopt(pycurl.PROXYTYPE, proxyType) 81 | if verbose : 82 | self.pco.setopt(pycurl.VERBOSE, 1) 83 | if debug : 84 | print('PyCurl version info:') 85 | print(pycurl.version_info()) 86 | print() 87 | self.pco.setopt(pycurl.DEBUGFUNCTION, self.debug) 88 | return 89 | 90 | def perform(self, url, fields=None, headers=None) : 91 | if fields : 92 | # This is a POST and we have fields to handle 93 | fields = urllib.urlencode(fields) 94 | self.pco.setopt(pycurl.POST, 1) 95 | self.pco.setopt(pycurl.POSTFIELDS, fields) 96 | else : 97 | # This is a GET, and we do nothing with fields 98 | pass 99 | pageContents = StringIO() 100 | self.pco.setopt(pycurl.WRITEFUNCTION, pageContents.write) 101 | self.pco.setopt(pycurl.URL, url) 102 | if headers : 103 | self.pco.setopt(pycurl.HTTPHEADER, headers) 104 | self.pco.perform() 105 | self.pco.close() 106 | self.pc = pageContents 107 | return 108 | 109 | def results(self) : 110 | # return the page contents that were received in the most recent perform() 111 | # self.pc is a StringIO object 112 | self.pc.seek(0) 113 | return self.pc 114 | 115 | def debug(self, debug_type, debug_msg) : 116 | print('debug(%d): %s' % (debug_type, debug_msg)) 117 | return 118 | 119 | try: 120 | # only call this once in a process. see libcurl docs for more info. 121 | pycurl.global_init(pycurl.GLOBAL_ALL) 122 | except: 123 | print('Fatal error: call to pycurl.global_init() failed for some reason') 124 | sys.exit(1) 125 | -------------------------------------------------------------------------------- /linode/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjfontaine/linode-python/a047859be78cf96ef1fbcae41880c0848f1fe53f/linode/__init__.py -------------------------------------------------------------------------------- /linode/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # vim:ts=2:sw=2:expandtab 3 | """ 4 | A Python library to perform low-level Linode API functions. 5 | 6 | Copyright (c) 2010 Timothy J Fontaine 7 | Copyright (c) 2010 Josh Wright 8 | Copyright (c) 2010 Ryan Tucker 9 | Copyright (c) 2008 James C Sinclair 10 | Copyright (c) 2013 Tim Heckman 11 | Copyright (c) 2014 Magnus Appelquist 12 | 13 | Permission is hereby granted, free of charge, to any person 14 | obtaining a copy of this software and associated documentation 15 | files (the "Software"), to deal in the Software without 16 | restriction, including without limitation the rights to use, 17 | copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following 20 | conditions: 21 | 22 | The above copyright notice and this permission notice shall be 23 | included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | OTHER DEALINGS IN THE SOFTWARE. 33 | """ 34 | 35 | from decimal import Decimal 36 | import logging 37 | import urllib 38 | import urllib2 39 | import copy 40 | 41 | try: 42 | import json 43 | FULL_BODIED_JSON = True 44 | except: 45 | import simplejson as json 46 | FULL_BODIED_JSON = False 47 | 48 | try: 49 | import requests 50 | from types import MethodType 51 | 52 | def requests_request(url, fields, headers): 53 | return requests.Request(method="POST", url=url, headers=headers, data=fields) 54 | 55 | def requests_open(request): 56 | r = request.prepare() 57 | s = requests.Session() 58 | s.verify = True 59 | response = s.send(r) 60 | response.read = MethodType(lambda x: x.text, response) 61 | return response 62 | 63 | URLOPEN = requests_open 64 | URLREQUEST = requests_request 65 | except: 66 | try: 67 | import VEpycurl 68 | def vepycurl_request(url, fields, headers): 69 | return (url, fields, headers) 70 | 71 | def vepycurl_open(request): 72 | c = VEpycurl.VEpycurl(verifySSL=2) 73 | url, fields, headers = request 74 | nh = [ '%s: %s' % (k, v) for k,v in headers.items()] 75 | c.perform(url, fields, nh) 76 | return c.results() 77 | 78 | URLOPEN = vepycurl_open 79 | URLREQUEST = vepycurl_request 80 | except: 81 | import warnings 82 | ssl_message = 'using urllib instead of pycurl, urllib does not verify SSL remote certificates, there is a risk of compromised communication' 83 | warnings.warn(ssl_message, RuntimeWarning) 84 | 85 | def urllib_request(url, fields, headers): 86 | fields = urllib.urlencode(fields) 87 | return urllib2.Request(url, fields, headers) 88 | 89 | URLOPEN = urllib2.urlopen 90 | URLREQUEST = urllib_request 91 | 92 | 93 | class MissingRequiredArgument(Exception): 94 | """Raised when a required parameter is missing.""" 95 | 96 | def __init__(self, value): 97 | self.value = value 98 | def __str__(self): 99 | return repr(self.value) 100 | def __reduce__(self): 101 | return (self.__class__, (self.value, )) 102 | 103 | class ApiError(Exception): 104 | """Raised when a Linode API call returns an error. 105 | 106 | Returns: 107 | [{u'ERRORCODE': Error code number, 108 | u'ERRORMESSAGE': 'Description of error'}] 109 | 110 | ErrorCodes that can be returned by any method, per Linode API specification: 111 | 0: ok 112 | 1: Bad request 113 | 2: No action was requested 114 | 3: The requested class does not exist 115 | 4: Authentication failed 116 | 5: Object not found 117 | 6: A required property is missing for this action 118 | 7: Property is invalid 119 | 8: A data validation error has occurred 120 | 9: Method Not Implemented 121 | 10: Too many batched requests 122 | 11: RequestArray isn't valid JSON or WDDX 123 | 13: Permission denied 124 | 30: Charging the credit card failed 125 | 31: Credit card is expired 126 | 40: Limit of Linodes added per hour reached 127 | 41: Linode must have no disks before delete 128 | """ 129 | 130 | def __init__(self, value): 131 | self.value = value 132 | def __str__(self): 133 | return repr(self.value) 134 | def __reduce__(self): 135 | return (self.__class__, (self.value, )) 136 | 137 | class ApiInfo: 138 | valid_commands = {} 139 | valid_params = {} 140 | 141 | LINODE_API_URL = 'https://api.linode.com/api/' 142 | 143 | VERSION = '0.0.1' 144 | 145 | class LowerCaseDict(dict): 146 | def __init__(self, copy=None): 147 | if copy: 148 | if isinstance(copy, dict): 149 | for k,v in copy.items(): 150 | dict.__setitem__(self, k.lower(), v) 151 | else: 152 | for k,v in copy: 153 | dict.__setitem__(self, k.lower(), v) 154 | 155 | def __getitem__(self, key): 156 | return dict.__getitem__(self, key.lower()) 157 | 158 | def __setitem__(self, key, value): 159 | dict.__setitem__(self, key.lower(), value) 160 | 161 | def __contains__(self, key): 162 | return dict.__contains__(self, key.lower()) 163 | 164 | def get(self, key, def_val=None): 165 | return dict.get(self, key.lower(), def_val) 166 | 167 | def setdefault(self, key, def_val=None): 168 | return dict.setdefault(self, key.lower(), def_val) 169 | 170 | def update(self, copy): 171 | for k,v in copy.items(): 172 | dict.__setitem__(self, k.lower(), v) 173 | 174 | def fromkeys(self, iterable, value=None): 175 | d = self.__class__() 176 | for k in iterable: 177 | dict.__setitem__(d, k.lower(), value) 178 | return d 179 | 180 | def pop(self, key, def_val=None): 181 | return dict.pop(self, key.lower(), def_val) 182 | 183 | class Api: 184 | """Linode API (version 2) client class. 185 | 186 | Instantiate with: Api(), or Api(optional parameters) 187 | 188 | Optional parameters: 189 | key - Your API key, from "My Profile" in the LPM (default: None) 190 | batching - Enable batching support (default: False) 191 | 192 | This interfaces with the Linode API (version 2) and receives a response 193 | via JSON, which is then parsed and returned as a dictionary (or list 194 | of dictionaries). 195 | 196 | In the event of API problems, raises ApiError: 197 | api.ApiError: [{u'ERRORCODE': 99, 198 | u'ERRORMESSAGE': u'Error Message'}] 199 | 200 | If you do not specify a key, the only method you may use is 201 | user_getapikey(username, password). This will retrieve and store 202 | the API key for a given user. 203 | 204 | Full documentation on the API can be found from Linode at: 205 | http://www.linode.com/api/ 206 | """ 207 | 208 | def __init__(self, key=None, batching=False): 209 | self.__key = key 210 | self.__urlopen = URLOPEN 211 | self.__request = URLREQUEST 212 | self.batching = batching 213 | self.__batch_cache = [] 214 | 215 | @staticmethod 216 | def valid_commands(): 217 | """Returns a list of API commands supported by this class.""" 218 | return list(ApiInfo.valid_commands.keys()) 219 | 220 | @staticmethod 221 | def valid_params(): 222 | """Returns a list of all parameters used by methods of this class.""" 223 | return list(ApiInfo.valid_params.keys()) 224 | 225 | def batchFlush(self): 226 | """Initiates a batch flush. Raises Exception if not in batching mode.""" 227 | if not self.batching: 228 | raise Exception('Cannot flush requests when not batching') 229 | 230 | s = json.dumps(self.__batch_cache) 231 | self.__batch_cache = [] 232 | request = { 'api_action' : 'batch', 'api_requestArray' : s } 233 | return self.__send_request(request) 234 | 235 | def __getattr__(self, name): 236 | """Return a callable for any undefined attribute and assume it's an API call""" 237 | if name.startswith('__'): 238 | raise AttributeError() 239 | 240 | def generic_request(*args, **kw): 241 | request = LowerCaseDict(kw) 242 | request['api_action'] = name.replace('_', '.') 243 | 244 | if self.batching: 245 | self.__batch_cache.append(request) 246 | logging.debug('Batched: %s', json.dumps(request)) 247 | else: 248 | return self.__send_request(request) 249 | 250 | generic_request.__name__ = name 251 | return generic_request 252 | 253 | def __send_request(self, request): 254 | if self.__key: 255 | request['api_key'] = self.__key 256 | elif request['api_action'] != 'user.getapikey': 257 | raise Exception('Must call user_getapikey to fetch key') 258 | 259 | request['api_responseFormat'] = 'json' 260 | 261 | request_log = copy.deepcopy(request) 262 | redact = ['api_key','rootsshkey','rootpass'] 263 | for r in redact: 264 | if r in request_log: 265 | request_log[r] = '{0}: xxxx REDACTED xxxx'.format(r) 266 | 267 | logging.debug('Parameters '+str(request_log)) 268 | #request = urllib.urlencode(request) 269 | 270 | headers = { 271 | 'User-Agent': 'LinodePython/'+VERSION, 272 | } 273 | 274 | req = self.__request(LINODE_API_URL, request, headers) 275 | response = self.__urlopen(req) 276 | response = response.read() 277 | 278 | logging.debug('Raw Response: '+response) 279 | 280 | if FULL_BODIED_JSON: 281 | try: 282 | s = json.loads(response, parse_float=Decimal) 283 | except Exception as ex: 284 | print(response) 285 | raise ex 286 | else: 287 | # Stuck with simplejson, which won't let us parse_float 288 | s = json.loads(response) 289 | 290 | if isinstance(s, dict): 291 | s = LowerCaseDict(s) 292 | if len(s['ERRORARRAY']) > 0: 293 | if s['ERRORARRAY'][0]['ERRORCODE'] is not 0: 294 | raise ApiError(s['ERRORARRAY']) 295 | if s['ACTION'] == 'user.getapikey': 296 | self.__key = s['DATA']['API_KEY'] 297 | logging.debug('API key is: '+self.__key) 298 | return s['DATA'] 299 | else: 300 | return s 301 | 302 | def __api_request(required=[], optional=[], returns=[]): 303 | """Decorator to define required and optional parameters""" 304 | for k in required: 305 | k = k.lower() 306 | if k not in ApiInfo.valid_params: 307 | ApiInfo.valid_params[k] = True 308 | 309 | for k in optional: 310 | k = k.lower() 311 | if k not in ApiInfo.valid_params: 312 | ApiInfo.valid_params[k] = True 313 | 314 | def decorator(func): 315 | if func.__name__ not in ApiInfo.valid_commands: 316 | ApiInfo.valid_commands[func.__name__] = True 317 | 318 | def wrapper(self, **kw): 319 | request = LowerCaseDict() 320 | request['api_action'] = func.__name__.replace('_', '.') 321 | 322 | params = LowerCaseDict(kw) 323 | 324 | for k in required: 325 | if k not in params: 326 | raise MissingRequiredArgument(k) 327 | 328 | for k in params: 329 | request[k] = params[k] 330 | 331 | result = func(self, request) 332 | 333 | if result is not None: 334 | request = result 335 | 336 | if self.batching: 337 | self.__batch_cache.append(request) 338 | logging.debug('Batched: '+ json.dumps(request)) 339 | else: 340 | return self.__send_request(request) 341 | 342 | wrapper.__name__ = func.__name__ 343 | wrapper.__doc__ = func.__doc__ 344 | wrapper.__dict__.update(func.__dict__) 345 | 346 | if (required or optional) and wrapper.__doc__: 347 | # Generate parameter documentation in docstring 348 | if len(wrapper.__doc__.split('\n')) is 1: # one-liners need whitespace 349 | wrapper.__doc__ += '\n' 350 | wrapper.__doc__ += '\n Keyword arguments (* = required):\n' 351 | wrapper.__doc__ += ''.join(['\t *%s\n' % p for p in required]) 352 | wrapper.__doc__ += ''.join(['\t %s\n' % p for p in optional]) 353 | 354 | if returns and wrapper.__doc__: 355 | # we either have a list of dicts or a just plain dict 356 | if len(wrapper.__doc__.split('\n')) is 1: # one-liners need whitespace 357 | wrapper.__doc__ += '\n' 358 | if isinstance(returns, list): 359 | width = max(len(q) for q in returns[0].keys()) 360 | wrapper.__doc__ += '\n Returns list of dictionaries:\n\t[{\n' 361 | wrapper.__doc__ += ''.join(['\t %-*s: %s\n' 362 | % (width, p, returns[0][p]) for p in returns[0].keys()]) 363 | wrapper.__doc__ += '\t }, ...]\n' 364 | else: 365 | width = max(len(q) for q in returns.keys()) 366 | wrapper.__doc__ += '\n Returns dictionary:\n\t {\n' 367 | wrapper.__doc__ += ''.join(['\t %-*s: %s\n' 368 | % (width, p, returns[p]) for p in returns.keys()]) 369 | wrapper.__doc__ += '\t }\n' 370 | 371 | return wrapper 372 | return decorator 373 | 374 | @__api_request(optional=['LinodeID'], 375 | returns=[{u'ALERT_BWIN_ENABLED': '0 or 1', 376 | u'ALERT_BWIN_THRESHOLD': 'integer (Mb/sec?)', 377 | u'ALERT_BWOUT_ENABLED': '0 or 1', 378 | u'ALERT_BWOUT_THRESHOLD': 'integer (Mb/sec?)', 379 | u'ALERT_BWQUOTA_ENABLED': '0 or 1', 380 | u'ALERT_BWQUOTA_THRESHOLD': '0..100', 381 | u'ALERT_CPU_ENABLED': '0 or 1', 382 | u'ALERT_CPU_THRESHOLD': '0..400 (% CPU)', 383 | u'ALERT_DISKIO_ENABLED': '0 or 1', 384 | u'ALERT_DISKIO_THRESHOLD': 'integer (IO ops/sec?)', 385 | u'BACKUPSENABLED': '0 or 1', 386 | u'BACKUPWEEKLYDAY': '0..6 (day of week, 0 = Sunday)', 387 | u'BACKUPWINDOW': 'some integer', 388 | u'DATACENTERID': 'Datacenter ID', 389 | u'LABEL': 'linode label', 390 | u'LINODEID': 'Linode ID', 391 | u'LPM_DISPLAYGROUP': 'group label', 392 | u'PLANID': 'plan id', 393 | u'STATUS': 'Status flag', 394 | u'TOTALHD': 'available disk (GB)', 395 | u'TOTALRAM': 'available RAM (MB)', 396 | u'TOTALXFER': 'available bandwidth (GB/month)', 397 | u'WATCHDOG': '0 or 1'}]) 398 | def linode_list(self, request): 399 | """List information about your Linodes. 400 | 401 | Status flag values: 402 | -2: Boot Failed (not in use) 403 | -1: Being Created 404 | 0: Brand New 405 | 1: Running 406 | 2: Powered Off 407 | 3: Shutting Down (not in use) 408 | 4: Saved to Disk (not in use) 409 | """ 410 | pass 411 | 412 | @__api_request(required=['LinodeID'], 413 | optional=['Label', 'lpm_displayGroup', 'Alert_cpu_enabled', 414 | 'Alert_cpu_threshold', 'Alert_diskio_enabled', 415 | 'Alert_diskio_threshold', 'Alert_bwin_enabled', 416 | 'Alert_bwin_threshold', 'Alert_bwout_enabled', 417 | 'Alert_bwout_threshold', 'Alert_bwquota_enabled', 418 | 'Alert_bwquota_threshold', 'backupWindow', 419 | 'backupWeeklyDay', 'watchdog'], 420 | returns={u'LinodeID': 'LinodeID'}) 421 | def linode_update(self, request): 422 | """Update information about, or settings for, a Linode. 423 | 424 | See linode_list.__doc__ for information on parameters. 425 | """ 426 | pass 427 | 428 | @__api_request(required=['LinodeID', 'DatacenterID', 'PlanID'], 429 | optional=['PaymentTerm'], 430 | returns={u'LinodeID': 'New Linode ID'}) 431 | def linode_clone(self, request): 432 | """Create a new Linode, then clone the specified LinodeID to the 433 | new Linode. It is recommended that the source Linode be powered 434 | down during the clone. 435 | 436 | This will create a billing event. 437 | """ 438 | pass 439 | 440 | @__api_request(required=['DatacenterID', 'PlanID'], 441 | optional=['PaymentTerm'], 442 | returns={u'LinodeID': 'New Linode ID'}) 443 | def linode_create(self, request): 444 | """Create a new Linode. 445 | 446 | This will create a billing event. 447 | """ 448 | pass 449 | 450 | @__api_request(required=['LinodeID'], returns={u'JobID': 'Job ID'}) 451 | def linode_shutdown(self, request): 452 | """Submit a shutdown job for a Linode. 453 | 454 | On job submission, returns the job ID. Does not wait for job 455 | completion (see linode_job_list). 456 | """ 457 | pass 458 | 459 | @__api_request(required=['LinodeID'], optional=['ConfigID'], 460 | returns={u'JobID': 'Job ID'}) 461 | def linode_boot(self, request): 462 | """Submit a boot job for a Linode. 463 | 464 | On job submission, returns the job ID. Does not wait for job 465 | completion (see linode_job_list). 466 | """ 467 | pass 468 | 469 | @__api_request(required=['LinodeID'], optional=['skipChecks'], 470 | returns={u'LinodeID': 'Destroyed Linode ID'}) 471 | def linode_delete(self, request): 472 | """Completely, immediately, and totally deletes a Linode. 473 | Requires all disk images be deleted first, or that the optional 474 | skipChecks parameter be set. 475 | 476 | This will create a billing event. 477 | 478 | WARNING: Deleting your last Linode may disable services that 479 | require a paid account (e.g. DNS hosting). 480 | """ 481 | pass 482 | 483 | @__api_request(required=['LinodeID'], optional=['ConfigID'], 484 | returns={u'JobID': 'Job ID'}) 485 | def linode_reboot(self, request): 486 | """Submit a reboot job for a Linode. 487 | 488 | On job submission, returns the job ID. Does not wait for job 489 | completion (see linode_job_list). 490 | """ 491 | pass 492 | 493 | @__api_request(required=['LinodeID', 'PlanID']) 494 | def linode_resize(self, request): 495 | """Resize a Linode from one plan to another. 496 | 497 | Immediately shuts the Linode down, charges/credits the account, and 498 | issues a migration to an appropriate host server. 499 | """ 500 | pass 501 | 502 | @__api_request(required=['LinodeID'], 503 | returns=[{u'Comments': 'comments field', 504 | u'ConfigID': 'Config ID', 505 | u'DiskList': "',,,,,,,,' disk array", 506 | u'helper_depmod': '0 or 1', 507 | u'helper_disableUpdateDB': '0 or 1', 508 | u'helper_libtls': '0 or 1', 509 | u'helper_xen': '0 or 1', 510 | u'KernelID': 'Kernel ID', 511 | u'Label': 'Profile name', 512 | u'LinodeID': 'Linode ID', 513 | u'RAMLimit': 'Max memory (MB), 0 is unlimited', 514 | u'RootDeviceCustom': '', 515 | u'RootDeviceNum': 'root partition (1=first, 0=RootDeviceCustom)', 516 | u'RootDeviceRO': '0 or 1', 517 | u'RunLevel': "in ['default', 'single', 'binbash'"}]) 518 | def linode_config_list(self, request): 519 | """Lists all configuration profiles for a given Linode.""" 520 | pass 521 | 522 | @__api_request(required=['LinodeID', 'ConfigID'], 523 | optional=['KernelID', 'Label', 'Comments', 'RAMLimit', 524 | 'DiskList', 'RunLevel', 'RootDeviceNum', 525 | 'RootDeviceCustom', 'RootDeviceRO', 526 | 'helper_disableUpdateDB', 'helper_xen', 527 | 'helper_depmod'], 528 | returns={u'ConfigID': 'Config ID'}) 529 | def linode_config_update(self, request): 530 | """Updates a configuration profile.""" 531 | pass 532 | 533 | @__api_request(required=['LinodeID', 'KernelID', 'Label', 'Disklist'], 534 | optional=['Comments', 'RAMLimit', 'RunLevel', 535 | 'RootDeviceNum', 'RootDeviceCustom', 536 | 'RootDeviceRO', 'helper_disableUpdateDB', 537 | 'helper_xen', 'helper_depmod'], 538 | returns={u'ConfigID': 'Config ID'}) 539 | def linode_config_create(self, request): 540 | """Creates a configuration profile.""" 541 | pass 542 | 543 | @__api_request(required=['LinodeID', 'ConfigID'], 544 | returns={u'ConfigID': 'Config ID'}) 545 | def linode_config_delete(self, request): 546 | """Deletes a configuration profile. This does not delete the 547 | Linode itself, nor its disk images (see linode_disk_delete, 548 | linode_delete). 549 | """ 550 | pass 551 | 552 | @__api_request(required=['LinodeID'], 553 | returns=[{u'CREATE_DT': u'YYYY-MM-DD hh:mm:ss.0', 554 | u'DISKID': 'Disk ID', 555 | u'ISREADONLY': '0 or 1', 556 | u'LABEL': 'Disk label', 557 | u'LINODEID': 'Linode ID', 558 | u'SIZE': 'Size of disk (MB)', 559 | u'STATUS': 'Status flag', 560 | u'TYPE': "in ['ext4', 'ext3', 'swap', 'raw']", 561 | u'UPDATE_DT': u'YYYY-MM-DD hh:mm:ss.0'}]) 562 | def linode_disk_list(self, request): 563 | """Lists all disk images associated with a Linode.""" 564 | pass 565 | 566 | @__api_request(required=['LinodeID', 'DiskID'], 567 | optional=['Label', 'isReadOnly'], 568 | returns={u'DiskID': 'Disk ID'}) 569 | def linode_disk_update(self, request): 570 | """Updates the information about a disk image.""" 571 | pass 572 | 573 | @__api_request(required=['LinodeID', 'Type', 'Size', 'Label'], 574 | optional=['isReadOnly'], 575 | returns={u'DiskID': 'Disk ID', u'JobID': 'Job ID'}) 576 | def linode_disk_create(self, request): 577 | """Submits a job to create a new disk image. 578 | 579 | On job submission, returns the disk ID and job ID. Does not 580 | wait for job completion (see linode_job_list). 581 | """ 582 | pass 583 | 584 | @__api_request(required=['LinodeID', 'DiskID'], 585 | returns={u'DiskID': 'New Disk ID', u'JobID': 'Job ID'}) 586 | def linode_disk_duplicate(self, request): 587 | """Submits a job to preform a bit-for-bit copy of a disk image. 588 | 589 | On job submission, returns the disk ID and job ID. Does not 590 | wait for job completion (see linode_job_list). 591 | """ 592 | pass 593 | 594 | @__api_request(required=['LinodeID', 'DiskID'], 595 | returns={u'DiskID': 'Deleted Disk ID', u'JobID': 'Job ID'}) 596 | def linode_disk_delete(self, request): 597 | """Submits a job to delete a disk image. 598 | 599 | WARNING: All data on the disk image will be lost forever. 600 | 601 | On job submission, returns the disk ID and job ID. Does not 602 | wait for job completion (see linode_job_list). 603 | """ 604 | pass 605 | 606 | @__api_request(required=['LinodeID', 'DiskID', 'Size'], 607 | returns={u'DiskID': 'Disk ID', u'JobID': 'Job ID'}) 608 | def linode_disk_resize(self, request): 609 | """Submits a job to resize a partition. 610 | 611 | On job submission, returns the disk ID and job ID. Does not 612 | wait for job completion (see linode_job_list). 613 | """ 614 | pass 615 | 616 | @__api_request(required=['LinodeID', 'DistributionID', 'rootPass', 'Label', 617 | 'Size'], 618 | optional=['rootSSHKey'], 619 | returns={u'DiskID': 'New Disk ID', u'JobID': 'Job ID'}) 620 | def linode_disk_createfromdistribution(self, request): 621 | """Submits a job to create a disk image from a Linode template. 622 | 623 | On job submission, returns the disk ID and job ID. Does not 624 | wait for job completion (see linode_job_list). 625 | """ 626 | pass 627 | 628 | @__api_request(required=['LinodeID', 'StackScriptID', 'StackScriptUDFResponses', 629 | 'DistributionID', 'rootPass', 'Label', 'Size'], 630 | returns={u'DiskID': 'New Disk ID', u'JobID': 'Job ID'}) 631 | def linode_disk_createfromstackscript(self, request): 632 | """Submits a job to create a disk image from a Linode template. 633 | 634 | On job submission, returns the disk ID and job ID. Does not 635 | wait for job completion (see linode_job_list). Note: the 636 | 'StackScriptUDFResponses' must be a valid JSON string. 637 | """ 638 | pass 639 | 640 | @__api_request(required=['LinodeID'], 641 | returns={u'IPAddressID': 'New IP Address ID'}) 642 | def linode_ip_addprivate(self, request): 643 | """Assigns a Private IP to a Linode. Returns the IPAddressID 644 | that was added.""" 645 | pass 646 | 647 | @__api_request(required=['LinodeID'], 648 | returns={u'IPADDRESSID': 'New IP Address ID', 649 | u'IPADDRESS': '192.168.100.1'}) 650 | def linode_ip_addpublic(self, request): 651 | """Assigns a Public IP to a Linode. Returns the IPAddressID 652 | that was added.""" 653 | pass 654 | 655 | @__api_request(optional=['IPAddressID', 'LinodeID'], 656 | returns=[{u'ISPUBLIC': '0 or 1', 657 | u'IPADDRESS': '192.168.100.1', 658 | u'IPADDRESSID': 'IP address ID', 659 | u'LINODEID': 'Linode ID', 660 | u'RDNS_NAME': 'reverse.dns.name.here'}]) 661 | def linode_ip_list(self, request): 662 | """Lists a Linode's IP addresses.""" 663 | pass 664 | 665 | @__api_request(required=['IPAddressID','Hostname'], 666 | returns=[{u'HOSTNAME': 'reverse.dns.name.here', 667 | u'IPADDRESS': '192.168.100.1', 668 | u'IPADDRESSID': 'IP address ID'}]) 669 | def linode_ip_setrdns(self, request): 670 | """Sets the reverse DNS name of a public Linode IP.""" 671 | pass 672 | 673 | @__api_request(required=['IPAddressID'], 674 | optional=['withIPAddressID', 'toLinodeID'], 675 | returns=[{u'LINODEID': 'The ID of the Linode', 676 | u'IPADDRESS': '192.168.100.1', 677 | u'IPADDRESSID': 'IP address ID'}]) 678 | def linode_ip_swap(self, request): 679 | """Exchanges Public IP addresses between two Linodes within a Datacenter""" 680 | pass 681 | 682 | @__api_request(required=['LinodeID'], optional=['pendingOnly', 'JobID'], 683 | returns=[{u'ACTION': "API action (e.g. u'linode.create')", 684 | u'DURATION': "Duration spent processing or ''", 685 | u'ENTERED_DT': 'yyyy-mm-dd hh:mm:ss.0', 686 | u'HOST_FINISH_DT': "'yyyy-mm-dd hh:mm:ss.0' or ''", 687 | u'HOST_MESSAGE': 'response from host', 688 | u'HOST_START_DT': "'yyyy-mm-dd hh:mm:ss.0' or ''", 689 | u'HOST_SUCCESS': "1 or ''", 690 | u'JOBID': 'Job ID', 691 | u'LABEL': 'Description of job', 692 | u'LINODEID': 'Linode ID'}]) 693 | def linode_job_list(self, request): 694 | """Returns the contents of the job queue.""" 695 | pass 696 | 697 | @__api_request(optional=['isXen'], 698 | returns=[{u'ISXEN': '0 or 1', 699 | u'KERNELID': 'Kernel ID', 700 | u'LABEL': 'kernel version string'}]) 701 | def avail_kernels(self, request): 702 | """List available kernels.""" 703 | pass 704 | 705 | @__api_request(returns=[{u'CREATE_DT': 'YYYY-MM-DD hh:mm:ss.0', 706 | u'DISTRIBUTIONID': 'Distribution ID', 707 | u'IS64BIT': '0 or 1', 708 | u'LABEL': 'Description of image', 709 | u'MINIMAGESIZE': 'MB required to deploy image'}]) 710 | def avail_distributions(self, request): 711 | """Returns a list of available Linux Distributions.""" 712 | pass 713 | 714 | @__api_request(returns=[{u'DATACENTERID': 'Datacenter ID', 715 | u'LOCATION': 'City, ST, USA'}]) 716 | def avail_datacenters(self, request): 717 | """Returns a list of Linode data center facilities.""" 718 | pass 719 | 720 | @__api_request(returns=[{u'DISK': 'Maximum disk allocation (GB)', 721 | u'LABEL': 'Name of plan', 722 | u'PLANID': 'Plan ID', 723 | u'PRICE': 'Monthly price (US dollars)', 724 | u'HOURLY': 'Hourly price (US dollars)', 725 | u'RAM': 'Maximum memory (MB)', 726 | u'XFER': 'Allowed transfer (GB/mo)', 727 | u'AVAIL': {u'Datacenter ID': 'Quantity'}}]) 728 | def avail_linodeplans(self, request): 729 | """Returns a structure of Linode PlanIDs containing PlanIDs, and 730 | their availability in each datacenter. 731 | 732 | This plan is deprecated and will be removed in the future. 733 | """ 734 | pass 735 | 736 | @__api_request(optional=['StackScriptID', 'DistributionID', 'DistributionVendor', 737 | 'keywords'], 738 | returns=[{u'CREATE_DT': "'yyyy-mm-dd hh:mm:ss.0'", 739 | u'DEPLOYMENTSACTIVE': 'The number of Scripts that Depend on this Script', 740 | u'REV_DT': "'yyyy-mm-dd hh:mm:ss.0'", 741 | u'DESCRIPTION': 'User defined description of the script', 742 | u'SCRIPT': 'The actual source of the script', 743 | u'ISPUBLIC': '0 or 1', 744 | u'REV_NOTE': 'Comment regarding this revision', 745 | u'LABEL': 'test', 746 | u'LATESTREV': 'The number of the latest revision', 747 | u'DEPLOYMENTSTOTAL': 'Number of times this script has been deployed', 748 | u'STACKSCRIPTID': 'StackScript ID', 749 | u'DISTRIBUTIONIDLIST': 'Comma separated list of distributions this script is available'}]) 750 | def avail_stackscripts(self, request): 751 | """Returns a list of publicly available StackScript. 752 | """ 753 | pass 754 | 755 | @__api_request(required=['username', 'password'], 756 | returns={u'API_KEY': 'API key', u'USERNAME': 'Username'}) 757 | def user_getapikey(self, request): 758 | """Given a username and password, returns the user's API key. The 759 | key is remembered by this instance for future use. 760 | 761 | Please be advised that this will replace any previous key stored 762 | by the instance. 763 | """ 764 | pass 765 | 766 | @__api_request(optional=['DomainID'], 767 | returns=[{u'STATUS': 'Status flag', 768 | u'RETRY_SEC': 'SOA Retry field', 769 | u'DOMAIN': 'Domain name', 770 | u'DOMAINID': 'Domain ID number', 771 | u'DESCRIPTION': 'Description', 772 | u'MASTER_IPS': 'Master nameservers (for slave zones)', 773 | u'SOA_EMAIL': 'SOA e-mail address (user@domain)', 774 | u'REFRESH_SEC': 'SOA Refresh field', 775 | u'TYPE': 'Type of zone (master or slave)', 776 | u'EXPIRE_SEC': 'SOA Expire field', 777 | u'TTL_SEC': 'Default TTL'}]) 778 | def domain_list(self, request): 779 | """Returns a list of domains associated with this account.""" 780 | pass 781 | 782 | @__api_request(required=['DomainID'], 783 | returns={u'DomainID': 'Domain ID number'}) 784 | def domain_delete(self, request): 785 | """Deletes a given domain, by domainid.""" 786 | pass 787 | 788 | @__api_request(required=['Domain', 'Type'], 789 | optional=['SOA_Email', 'Refresh_sec', 'Retry_sec', 790 | 'Expire_sec', 'TTL_sec', 'status', 'master_ips'], 791 | returns={u'DomainID': 'Domain ID number'}) 792 | def domain_create(self, request): 793 | """Create a new domain. 794 | 795 | For type='master', SOA_Email is required. 796 | For type='slave', Master_IPs is required. 797 | 798 | Master_IPs is a comma or semicolon-delimited list of master IPs. 799 | Status is 1 (Active), 2 (EditMode), or 3 (Off). 800 | 801 | TTL values are rounded up to the nearest valid value: 802 | 300, 3600, 7200, 14400, 28800, 57600, 86400, 172800, 803 | 345600, 604800, 1209600, or 2419200 seconds. 804 | """ 805 | pass 806 | 807 | @__api_request(required=['DomainID'], 808 | optional=['Domain', 'Type', 'SOA_Email', 'Refresh_sec', 809 | 'Retry_sec', 'Expire_sec', 'TTL_sec', 'status', 810 | 'master_ips'], 811 | returns={u'DomainID': 'Domain ID number'}) 812 | def domain_update(self, request): 813 | """Updates the parameters of a given domain. 814 | 815 | TTL values are rounded up to the nearest valid value: 816 | 300, 3600, 7200, 14400, 28800, 57600, 86400, 172800, 817 | 345600, 604800, 1209600, or 2419200 seconds. 818 | """ 819 | pass 820 | 821 | @__api_request(required=['DomainID'], optional=['ResourceID'], 822 | returns=[{u'DOMAINID': 'Domain ID number', 823 | u'PROTOCOL': 'Protocol (for SRV)', 824 | u'TTL_SEC': 'TTL for record (0=default)', 825 | u'WEIGHT': 'Weight (for SRV)', 826 | u'NAME': 'The hostname or FQDN', 827 | u'RESOURCEID': 'Resource ID number', 828 | u'PRIORITY': 'Priority (for MX, SRV)', 829 | u'TYPE': 'Resource Type (A, MX, etc)', 830 | u'PORT': 'Port (for SRV)', 831 | u'TARGET': 'The "right hand side" of the record'}]) 832 | def domain_resource_list(self, request): 833 | """List the resources associated with a given DomainID.""" 834 | pass 835 | 836 | @__api_request(required=['DomainID', 'Type'], 837 | optional=['Name', 'Target', 'Priority', 'Weight', 838 | 'Port', 'Protocol', 'TTL_Sec'], 839 | returns={u'ResourceID': 'Resource ID number'}) 840 | def domain_resource_create(self, request): 841 | """Creates a resource within a given DomainID. 842 | 843 | TTL values are rounded up to the nearest valid value: 844 | 300, 3600, 7200, 14400, 28800, 57600, 86400, 172800, 845 | 345600, 604800, 1209600, or 2419200 seconds. 846 | 847 | For A and AAAA records, specify Target as "[remote_addr]" to 848 | use the source IP address of the request as the target, e.g. 849 | for updating pointers to dynamic IP addresses. 850 | """ 851 | pass 852 | 853 | @__api_request(required=['DomainID', 'ResourceID'], 854 | returns={u'ResourceID': 'Resource ID number'}) 855 | def domain_resource_delete(self, request): 856 | """Deletes a Resource from a Domain.""" 857 | pass 858 | 859 | @__api_request(required=['DomainID', 'ResourceID'], 860 | optional=['Name', 'Target', 'Priority', 'Weight', 'Port', 861 | 'Protocol', 'TTL_Sec'], 862 | returns={u'ResourceID': 'Resource ID number'}) 863 | def domain_resource_update(self, request): 864 | """Updates a domain resource. 865 | 866 | TTL values are rounded up to the nearest valid value: 867 | 300, 3600, 7200, 14400, 28800, 57600, 86400, 172800, 868 | 345600, 604800, 1209600, or 2419200 seconds. 869 | 870 | For A and AAAA records, specify Target as "[remote_addr]" to 871 | use the source IP address of the request as the target, e.g. 872 | for updating pointers to dynamic IP addresses. 873 | """ 874 | pass 875 | 876 | @__api_request(optional=['NodeBalancerID'], 877 | returns=[{u'ADDRESS4': 'IPv4 IP address of the NodeBalancer', 878 | u'ADDRESS6': 'IPv6 IP address of the NodeBalancer', 879 | u'CLIENTCONNTHROTTLE': 'Allowed connections per second, per client IP', 880 | u'HOSTNAME': 'NodeBalancer hostname', 881 | u'LABEL': 'NodeBalancer label', 882 | u'NODEBALANCERID': 'NodeBalancer ID', 883 | u'STATUS': 'NodeBalancer status, as a string'}]) 884 | def nodebalancer_list(self, request): 885 | """List information about your NodeBalancers.""" 886 | pass 887 | 888 | @__api_request(required=['NodeBalancerID'], 889 | optional=['Label', 890 | 'ClientConnThrottle'], 891 | returns={u'NodeBalancerID': 'NodeBalancerID'}) 892 | def nodebalancer_update(self, request): 893 | """Update information about, or settings for, a Nodebalancer. 894 | 895 | See nodebalancer_list.__doc__ for information on parameters. 896 | """ 897 | pass 898 | 899 | @__api_request(required=['DatacenterID', 'PaymentTerm'], 900 | returns={u'NodeBalancerID' : 'ID of the created NodeBalancer'}) 901 | def nodebalancer_create(self, request): 902 | """Creates a NodeBalancer.""" 903 | pass 904 | 905 | @__api_request(required=['NodeBalancerID'], 906 | returns={u'NodeBalancerID': 'Destroyed NodeBalancer ID'}) 907 | def nodebalancer_delete(self, request): 908 | """Immediately removes a NodeBalancer from your account and issues 909 | a pro-rated credit back to your account, if applicable.""" 910 | pass 911 | 912 | @__api_request(required=['NodeBalancerID'], 913 | optional=['ConfigID'], 914 | returns=[{ 915 | u'ALGORITHM': 'Balancing algorithm.', 916 | u'CHECK': 'Type of health check to perform.', 917 | u'CHECK_ATTEMPTS': 'Number of failed probes allowed.', 918 | u'CHECK_BODY': 'A regex against the expected result body.', 919 | u'CHECK_INTERVAL': 'Seconds between health check probes.', 920 | u'CHECK_PATH': 'The path of the health check request.', 921 | u'CHECK_TIMEOUT': 'Seconds to wait before calling a failure.', 922 | u'CONFIGID': 'ID of this config', 923 | u'NODEBALANCERID': 'NodeBalancer ID.', 924 | u'PORT': 'Port to bind to on public interface.', 925 | u'PROTOCOL': 'The protocol to be used (tcp or http).', 926 | u'STICKINESS': 'Session persistence.'}]) 927 | def nodebalancer_config_list(self, request): 928 | """List information about your NodeBalancer Configs.""" 929 | pass 930 | 931 | @__api_request(required=['ConfigID'], 932 | optional=['Algorithm', 'check', 'check_attempts', 'check_body', 933 | 'check_interval', 'check_path', 'check_timeout', 934 | 'Port', 'Protocol', 'Stickiness'], 935 | returns={u'ConfigID': 'The ConfigID you passed in the first place.'}) 936 | def nodebalancer_config_update(self, request): 937 | """Update information about, or settings for, a Nodebalancer Config. 938 | 939 | See nodebalancer_config_list.__doc__ for information on parameters. 940 | """ 941 | pass 942 | 943 | @__api_request(required=['NodeBalancerID'], 944 | optional=['Algorithm', 'check', 'check_attempts', 'check_body', 945 | 'check_interval', 'check_path', 'check_timeout', 946 | 'Port', 'Protocol', 'Stickiness'], 947 | returns={u'ConfigID': 'The ConfigID of the new Config.'}) 948 | def nodebalancer_config_create(self, request): 949 | """Create a Nodebalancer Config. 950 | 951 | See nodebalancer_config_list.__doc__ for information on parameters. 952 | """ 953 | pass 954 | 955 | @__api_request(required=['ConfigID'], 956 | returns={u'ConfigID': 'Destroyed Config ID'}) 957 | def nodebalancer_config_delete(self, request): 958 | """Deletes a NodeBalancer's Config.""" 959 | pass 960 | 961 | @__api_request(required=['ConfigID'], 962 | optional=['NodeID'], 963 | returns=[{u'ADDRESS': 'Address:port combination for the node.', 964 | u'CONFIGID': 'ConfigID of this node\'s config.', 965 | u'LABEL': 'The backend node\'s label.', 966 | u'MODE': 'Connection mode for this node.', 967 | u'NODEBALANCERID': 'ID of this node\'s nodebalancer.', 968 | u'NODEID': 'NodeID.', 969 | u'STATUS': 'Node\'s status in the nodebalancer.', 970 | u'WEIGHT': 'Load balancing weight.'}]) 971 | def nodebalancer_node_list(self, request): 972 | """List information about your NodeBalancer Nodes.""" 973 | pass 974 | 975 | @__api_request(required=['NodeID'], 976 | optional=['Label', 'Address', 'Weight', 'Mode'], 977 | returns={u'NodeID': 'The NodeID you passed in the first place.'}) 978 | def nodebalancer_node_update(self, request): 979 | """Update information about, or settings for, a Nodebalancer Node. 980 | 981 | See nodebalancer_node_list.__doc__ for information on parameters. 982 | """ 983 | pass 984 | 985 | @__api_request(required=['ConfigID', 'Label', 'Address'], 986 | optional=['Weight', 'Mode'], 987 | returns={u'NodeID': 'The NodeID of the new Node.'}) 988 | def nodebalancer_node_create(self, request): 989 | """Create a Nodebalancer Node. 990 | 991 | See nodebalancer_node_list.__doc__ for information on parameters. 992 | """ 993 | pass 994 | 995 | @__api_request(required=['NodeID'], 996 | returns={u'NodeID': 'Destroyed Node ID'}) 997 | def nodebalancer_node_delete(self, request): 998 | """Deletes a NodeBalancer Node.""" 999 | pass 1000 | 1001 | @__api_request(optional=['StackScriptID'], 1002 | returns=[{u'CREATE_DT': "'yyyy-mm-dd hh:mm:ss.0'", 1003 | u'DEPLOYMENTSACTIVE': 'The number of Scripts that Depend on this Script', 1004 | u'REV_DT': "'yyyy-mm-dd hh:mm:ss.0'", 1005 | u'DESCRIPTION': 'User defined description of the script', 1006 | u'SCRIPT': 'The actual source of the script', 1007 | u'ISPUBLIC': '0 or 1', 1008 | u'REV_NOTE': 'Comment regarding this revision', 1009 | u'LABEL': 'test', 1010 | u'LATESTREV': 'The number of the latest revision', 1011 | u'DEPLOYMENTSTOTAL': 'Number of times this script has been deployed', 1012 | u'STACKSCRIPTID': 'StackScript ID', 1013 | u'DISTRIBUTIONIDLIST': 'Comma separated list of distributions this script is available'}]) 1014 | def stackscript_list(self, request): 1015 | """List StackScripts you have created. 1016 | """ 1017 | pass 1018 | 1019 | @__api_request(required=['Label', 'DistributionIDList', 'script'], 1020 | optional=['Description', 'isPublic', 'rev_note'], 1021 | returns={'STACKSCRIPTID' : 'ID of the created StackScript'}) 1022 | def stackscript_create(self, request): 1023 | """Create a StackScript 1024 | """ 1025 | pass 1026 | 1027 | @__api_request(required=['StackScriptID'], 1028 | optional=['Label', 'Description', 'DistributionIDList', 1029 | 'isPublic', 'rev_note', 'script']) 1030 | def stackscript_update(self, request): 1031 | """Update an existing StackScript 1032 | """ 1033 | pass 1034 | 1035 | @__api_request(required=['StackScriptID']) 1036 | def stackscript_delete(self, request): 1037 | """Delete an existing StackScript 1038 | """ 1039 | pass 1040 | 1041 | @__api_request(returns=[{'Parameter' : 'Value'}]) 1042 | def test_echo(self, request): 1043 | """Echo back any parameters 1044 | """ 1045 | pass 1046 | 1047 | -------------------------------------------------------------------------------- /linode/deploy_abunch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A Script to deploy a bunch of Linodes from a given stackscript 4 | 5 | Copyright (c) 2011 Timothy J Fontaine 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | """ 28 | 29 | import json 30 | import logging 31 | import os.path 32 | import re 33 | import sys 34 | 35 | from optparse import OptionParser 36 | from getpass import getpass 37 | from os import environ, linesep 38 | 39 | import api 40 | 41 | parser = OptionParser() 42 | parser.add_option('-d', '--datacenter', dest="datacenter", 43 | help="datacenter to deploy to", metavar='DATACENTERID', 44 | action="store", type="int", 45 | ) 46 | parser.add_option('-c', '--count', dest="count", 47 | help="how many nodes to deploy", metavar="COUNT", 48 | action="store", type="int", 49 | ) 50 | parser.add_option('-s', '--stackscript', dest='stackscript', 51 | help='stackscript to deploy', metavar='STACKSCRIPTID', 52 | action='store', type='int', 53 | ) 54 | parser.add_option('-f', '--filename', dest='filename', 55 | help='filename with stackscript options', metavar='FILENAME', 56 | action='store', 57 | ) 58 | parser.add_option('-p', '--plan', dest='plan', 59 | help='linode plan that these nodes should be', metavar='PLANID', 60 | action='store', type='int', 61 | ) 62 | parser.add_option('-t', '--term', dest='term', 63 | help='payment term', metavar='TERM', 64 | action='store', type='choice', choices=('1','12','24'), 65 | ) 66 | parser.add_option('-D', '--distribution', dest='distribution', 67 | help='distribution to base deployment on', metavar='DISTRIBUTIONID', 68 | action='store', type='int', 69 | ) 70 | parser.add_option('-S', '--disksize', dest='disksize', 71 | help='size of the disk (in mb) that the stackscript should create', 72 | metavar='DISKSIZE', action='store', type='int', 73 | ) 74 | parser.add_option('-v', '--verbose', dest='verbose', 75 | help='enable debug logging in the api', action="store_true", 76 | default=False, 77 | ) 78 | parser.add_option('-k', '--kernel', dest='kernel', 79 | help='the kernel to assign to the configuration', metavar='KERNELID', 80 | action='store', type='int', 81 | ) 82 | parser.add_option('-B', '--boot', dest='boot', 83 | help='whether or not to issue a boot after a node is created', 84 | action='store_true', default=False, 85 | ) 86 | 87 | (options, args) = parser.parse_args() 88 | 89 | if options.verbose: 90 | logging.basicConfig(level=logging.DEBUG) 91 | 92 | try: 93 | if not options.count: 94 | raise Exception('Must specify how many nodes to create') 95 | 96 | if not options.datacenter: 97 | raise Exception('Must specify which datacenter to create nodes in') 98 | 99 | if not options.stackscript: 100 | raise Exception('Must specify which stackscript to deploy from') 101 | 102 | if not options.filename: 103 | raise Exception('Must specify filename of stackscript options') 104 | 105 | if not options.plan: 106 | raise Exception('Must specify the planid') 107 | 108 | if not options.term: 109 | raise Exception('Must speficy the payment term') 110 | 111 | if not options.distribution: 112 | raise Exception('Must speficy the distribution to deploy from') 113 | 114 | if not options.disksize: 115 | raise Exception('Must speficy the size of the disk to create') 116 | 117 | if not os.path.exists(options.filename): 118 | raise Exception('Options file must exist') 119 | 120 | if not options.kernel: 121 | raise Exception('Must specify a kernel to use for configuration') 122 | except Exception as ex: 123 | sys.stderr.write(str(ex) + linesep) 124 | parser.print_help() 125 | sys.exit('All options are required (yes I see the contradiction)') 126 | 127 | json_file = open(options.filename) 128 | 129 | # Round trip to make sure we are valid 130 | json_result = json.load(json_file) 131 | stackscript_options = json.dumps(json_result) 132 | 133 | if 'LINODE_API_KEY' in environ: 134 | api_key = environ['LINODE_API_KEY'] 135 | else: 136 | api_key = getpass('Enter API Key: ') 137 | 138 | print('Passwords must contain at least two of these four character classes: lower case letters - upper case letters - numbers - punctuation') 139 | root_pass = getpass('Enter the root password for all resulting nodes: ') 140 | root_pass2 = getpass('Re-Enter the root password: ') 141 | 142 | if root_pass != root_pass2: 143 | sys.exit('Passwords must match') 144 | 145 | valid_pass = 0 146 | 147 | if re.search(r'[A-Z]', root_pass): 148 | valid_pass += 1 149 | 150 | if re.search(r'[a-z]', root_pass): 151 | valid_pass += 1 152 | 153 | if re.search(r'[0-9]', root_pass): 154 | valid_pass += 1 155 | 156 | if re.search(r'\W', root_pass): 157 | valid_pass += 1 158 | 159 | if valid_pass < 2: 160 | sys.exit('Password too simple, only %d of 4 classes found' % (valid_pass)) 161 | 162 | linode_api = api.Api(api_key, batching=True) 163 | 164 | needFlush = False 165 | 166 | created_linodes = [] 167 | 168 | def deploy_set(): 169 | linode_order = [] 170 | for r in linode_api.batchFlush(): 171 | # TODO XXX FIXME handle error states 172 | linodeid = r['DATA']['LinodeID'] 173 | created_linodes.append(linodeid) 174 | linode_order.append(linodeid) 175 | linode_api.linode_disk_createfromstackscript( 176 | LinodeID=linodeid, 177 | StackScriptID=options.stackscript, 178 | StackScriptUDFResponses=stackscript_options, 179 | DistributionID=options.distribution, 180 | Label='From stackscript %d' % (options.stackscript), 181 | Size=options.disksize, 182 | rootPass=root_pass, 183 | ) 184 | to_boot = [] 185 | for r in linode_api.batchFlush(): 186 | # TODO XXX FIXME handle error states 187 | linodeid = linode_order.pop(0) 188 | diskid = [str(r['DATA']['DiskID'])] 189 | for i in range(8): diskid.append('') 190 | linode_api.linode_config_create( 191 | LinodeID=linodeid, 192 | KernelID=options.kernel, 193 | Label='From stackscript %d' % (options.stackscript), 194 | DiskList=','.join(diskid), 195 | ) 196 | if options.boot: 197 | to_boot.append(linodeid) 198 | linode_api.batchFlush() 199 | 200 | for l in to_boot: 201 | linode_api.linode_boot(LinodeID=l) 202 | 203 | if len(to_boot): 204 | linode_api.batchFlush() 205 | 206 | for i in range(options.count): 207 | if needFlush and i % 25 == 0: 208 | needFlush = False 209 | deploy_set() 210 | 211 | linode_api.linode_create( 212 | DatacenterID=options.datacenter, 213 | PlanID=options.plan, 214 | PaymentTerm=options.term, 215 | ) 216 | needFlush = True 217 | 218 | if needFlush: 219 | needFlush = False 220 | deploy_set() 221 | 222 | print('List of created Linodes:') 223 | print('[%s]' % (', '.join([str(l) for l in created_linodes]))) 224 | -------------------------------------------------------------------------------- /linode/fields.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | class Field(object): 4 | to_py = lambda self, value: value 5 | to_linode = to_py 6 | 7 | def __init__(self, field): 8 | self.field = field 9 | 10 | class IntField(Field): 11 | def to_py(self, value): 12 | if value is not None and value != '': 13 | return int(value) 14 | 15 | to_linode = to_py 16 | 17 | class FloatField(Field): 18 | def to_py(self, value): 19 | if value is not None: 20 | return float(value) 21 | 22 | to_linode = to_py 23 | 24 | class CharField(Field): 25 | to_py = lambda self, value: str(value) 26 | to_linode = to_py 27 | 28 | class BoolField(Field): 29 | def to_py(self, value): 30 | if value in (1, '1'): return True 31 | else: return False 32 | 33 | def to_linode(self, value): 34 | if value: return 1 35 | else: return 0 36 | 37 | class ChoiceField(Field): 38 | to_py = lambda self, value: value 39 | 40 | def __init__(self, field, choices=[]): 41 | Field.__init__(self, field) 42 | self.choices = choices 43 | 44 | def to_linode(self, value): 45 | if value in self.choices: 46 | return value 47 | else: 48 | raise AttributeError 49 | 50 | class ListField(Field): 51 | def __init__(self, field, type=Field(''), delim=','): 52 | Field.__init__(self, field) 53 | self.__type=type 54 | self.__delim=delim 55 | 56 | def to_linode(self, value): 57 | return self.__delim.join([str(self.__type.to_linode(v)) for v in value]) 58 | 59 | def to_py(self, value): 60 | return [self.__type.to_py(v) for v in value.split(self.__delim) if v != ''] 61 | 62 | class DateTimeField(Field): 63 | to_py = lambda self, value: datetime.strptime(value, '%Y-%m-%d %H:%M:%S.0') 64 | to_linode = lambda self, value: value.strftime('%Y-%m-%d %H:%M:%S.0') 65 | 66 | class ForeignField(Field): 67 | def __init__(self, field): 68 | self.field = field.primary_key 69 | self.__model = field 70 | 71 | def to_py(self, value): 72 | return self.__model.get(id=value) 73 | 74 | def to_linode(self, value): 75 | if isinstance(value, int): 76 | return value 77 | else: 78 | return value.id 79 | -------------------------------------------------------------------------------- /linode/methodcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | A quick script to verify that api.py is in sync with Linode's 4 | published list of methods. 5 | 6 | Copyright (c) 2010 Josh Wright 7 | Copyright (c) 2009 Ryan Tucker 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | """ 30 | 31 | #The list of subsections found in the API documentation. This should 32 | #probably be discovered automatically in the future 33 | api_subsections = ('linode', 'nodebalancer', 'stackscript', 'dns', 'utility') 34 | 35 | import api 36 | import re 37 | import itertools 38 | from HTMLParser import HTMLParser 39 | from urllib import unquote 40 | from urllib2 import urlopen 41 | 42 | class SubsectionParser(HTMLParser): 43 | base_url = 'http://www.linode.com/api/' 44 | 45 | def __init__(self, subsection): 46 | HTMLParser.__init__(self) 47 | self.subsection_re = re.compile('/api/%s/(.*)$' % subsection) 48 | self.methods = [] 49 | url = self.base_url + subsection 50 | req = urlopen(url) 51 | self.feed(req.read()) 52 | 53 | def handle_starttag(self, tag, attrs): 54 | if tag == 'a' and attrs: 55 | attr_dict = dict(attrs) 56 | match = self.subsection_re.match(attr_dict.get('href', '')) 57 | if match: 58 | self.methods.append(unquote(match.group(1)).replace('.','_')) 59 | 60 | local_methods = api.Api.valid_commands() 61 | remote_methods = list(itertools.chain(*[SubsectionParser(subsection).methods for subsection in api_subsections])) 62 | 63 | # Cross-check! 64 | for i in local_methods: 65 | if i not in remote_methods: 66 | print('REMOTE Missing: ' + i) 67 | for i in remote_methods: 68 | if i not in local_methods: 69 | print('LOCAL Missing: ' + i) 70 | 71 | -------------------------------------------------------------------------------- /linode/oop.py: -------------------------------------------------------------------------------- 1 | # vim:ts=2:sw=2:expandtab 2 | """ 3 | A Python library to provide level-level Linode API interaction. 4 | 5 | Copyright (c) 2010 Timothy J Fontaine 6 | Copyright (c) 2010 Josh Wright 7 | Copyright (c) 2010 Ryan Tucker 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | """ 30 | 31 | import logging 32 | 33 | from os import environ 34 | 35 | from api import Api, LowerCaseDict 36 | from fields import * 37 | 38 | _id_cache = {} 39 | 40 | ActiveContext = None 41 | 42 | class LinodeObject(object): 43 | fields = None 44 | update_method = None 45 | create_method = None 46 | primary_key = None 47 | list_method = None 48 | 49 | def __init__(self, entry={}): 50 | entry = dict([(str(k), v) for k,v in entry.items()]) 51 | self.__entry = LowerCaseDict(entry) 52 | 53 | def __getattr__(self, name): 54 | name = name.replace('_LinodeObject', '') 55 | if name == '__entry': 56 | return self.__dict__[name] 57 | elif name not in self.fields: 58 | raise AttributeError 59 | else: 60 | f= self.fields[name] 61 | value = None 62 | if f.field.lower() in self.__entry: 63 | value = self.__entry[f.field.lower()] 64 | return f.to_py(value) 65 | 66 | def __setattr__(self, name, value): 67 | name = name.replace('_LinodeObject', '') 68 | if name == '__entry': 69 | object.__setattr__(self, name, value) 70 | elif name not in self.fields: 71 | raise AttributeError 72 | else: 73 | f = self.fields[name] 74 | self.__entry[f.field.lower()] = f.to_linode(value) 75 | 76 | def __str__(self): 77 | s = [] 78 | for k,v in self.fields.items(): 79 | if v.field in self.__entry: 80 | value = v.to_py(self.__entry[v.field]) 81 | if isinstance(value, list): 82 | s.append('%s: [%s]' % (k, ', '.join([str(x) for x in value]))) 83 | else: 84 | s.append('%s: %s' % (k, str(value))) 85 | return '['+', '.join(s)+']' 86 | 87 | def save(self): 88 | if self.id: 89 | self.update() 90 | else: 91 | self.id = self.create_method(ActiveContext, **self.__entry)[self.primary_key] 92 | 93 | def update(self): 94 | self.update_method(ActiveContext, **self.__entry) 95 | 96 | @classmethod 97 | def __resolve_kwargs(self, kw): 98 | kwargs = {} 99 | for k, v in kw.items(): 100 | f = self.fields[k.lower()] 101 | kwargs[f.field] = f.to_linode(v) 102 | return kwargs 103 | 104 | @classmethod 105 | def list(self, **kw): 106 | kwargs = self.__resolve_kwargs(kw) 107 | 108 | """ 109 | if self not in _id_cache: 110 | _id_cache[self] = {} 111 | """ 112 | 113 | for l in self.list_method(ActiveContext, **kwargs): 114 | l = LowerCaseDict(l) 115 | o = self(l) 116 | o.cache_add() 117 | yield o 118 | 119 | @classmethod 120 | def get(self, **kw): 121 | kwargs = self.__resolve_kwargs(kw) 122 | 123 | """ 124 | if self not in _id_cache: 125 | _id_cache[self] = {} 126 | """ 127 | 128 | result = None 129 | """ 130 | for k,v in _id_cache[self].items(): 131 | found = True 132 | for i, j in kwargs.items(): 133 | if i not in v or v[i] != j: 134 | found = False 135 | break 136 | if not found: 137 | continue 138 | else: 139 | result = v 140 | break 141 | """ 142 | 143 | 144 | if not result: 145 | result = LowerCaseDict(self.list_method(ActiveContext, **kwargs)[0]) 146 | o = self(result) 147 | o.cache_add() 148 | return o 149 | else: 150 | return self(result) 151 | 152 | def cache_remove(self): 153 | pass 154 | """ 155 | del _id_cache[self.__class__][self.__entry[self.primary_key]] 156 | """ 157 | 158 | def cache_add(self): 159 | pass 160 | """ 161 | key = self.__class__ 162 | if key not in _id_cache: 163 | _id_cache[key] = {} 164 | 165 | _id_cache[key][self.__entry[self.primary_key]] = self.__entry 166 | """ 167 | 168 | class Datacenter(LinodeObject): 169 | fields = { 170 | 'id' : IntField('DatacenterID'), 171 | 'location' : CharField('Location'), 172 | 'name' : CharField('Location'), 173 | } 174 | 175 | list_method = Api.avail_datacenters 176 | primary_key = 'DatacenterID' 177 | 178 | class LinodePlan(LinodeObject): 179 | fields = { 180 | 'id' : IntField('PlanID'), 181 | 'label' : CharField('Label'), 182 | 'price' : FloatField('Price'), 183 | 'ram' : IntField('Ram'), 184 | 'xfer' : IntField('Xfer'), 185 | } 186 | 187 | list_method = Api.avail_linodeplans 188 | primary_key = 'PlanID' 189 | 190 | class Linode(LinodeObject): 191 | fields = { 192 | 'id' : IntField('LinodeID'), 193 | 'datacenter' : ForeignField(Datacenter), 194 | 'plan' : ForeignField(LinodePlan), 195 | 'term' : ChoiceField('PaymentTerm', choices=[1, 12, 24]), 196 | 'name' : CharField('Label'), 197 | 'label' : CharField('Label'), 198 | 'group' : Field('lpm_displayGroup'), 199 | 'cpu_enabled' : BoolField('Alert_cpu_enabled'), 200 | 'cpu_threshold' : IntField('Alert_cpu_threshold'), 201 | 'diskio_enabled' : BoolField('Alert_diskio_enabled'), 202 | 'diskio_threshold' : IntField('Alert_diskio_enabled'), 203 | 'bwin_enabled' : BoolField('Alert_bwin_enabled'), 204 | 'bwin_threshold' : IntField('Alert_bwin_threshold'), 205 | 'bwout_enabled' : BoolField('Alert_bwout_enabeld'), 206 | 'bwout_threshold' : IntField('Alert_bwout_threshold'), 207 | 'bwquota_enabled' : BoolField('Alert_bwquota_enabled'), 208 | 'bwquota_threshold' : IntField('Alert_bwquota_threshold'), 209 | 'backup_window' : IntField('backupWindow'), 210 | 'backup_weekly_day' : ChoiceField('backupWeeklyDay', choices=list(range(6))), 211 | 'watchdog' : BoolField('watchdog'), 212 | 'total_ram' : IntField('TotalRam'), 213 | 'total_diskspace' : IntField('TotalHD'), 214 | 'total_xfer' : IntField('TotalXfer'), 215 | 'status' : IntField('Status'), 216 | } 217 | 218 | update_method = Api.linode_update 219 | create_method = Api.linode_create 220 | primary_key = 'LinodeID' 221 | list_method = Api.linode_list 222 | 223 | def boot(self): 224 | ### TODO XXX FIXME return LinodeJob 225 | return ActiveContext.linode_boot(linodeid=self.id)['JobID'] 226 | 227 | def shutdown(self): 228 | ### TODO XXX FIXME return LinodeJob 229 | return ActiveContext.linode_shutdown(linodeid=self.id)['JobID'] 230 | 231 | def reboot(self): 232 | ### TODO XXX FIXME return LinodeJob 233 | return ActiveContext.linode_reboot(linodeid=self.id)['JobID'] 234 | 235 | def delete(self): 236 | ActiveContext.linode_delete(linodeid=self.id) 237 | self.cache_remove() 238 | 239 | class LinodeJob(LinodeObject): 240 | fields = { 241 | 'id' : IntField('JobID'), 242 | 'linode' : ForeignField(Linode), 243 | 'label' : CharField('Label'), 244 | 'name' : CharField('Label'), 245 | 'entered' : DateTimeField('ENTERED_DT'), 246 | 'started' : DateTimeField('HOST_START_DT'), 247 | 'finished' : DateTimeField('HOST_FINISH_DT'), 248 | 'message' : CharField('HOST_MESSAGE'), 249 | 'duration' : IntField('DURATION'), 250 | 'success' : BoolField('HOST_SUCCESS'), 251 | 'pending_only' : BoolField('PendingOnly'), 252 | } 253 | 254 | list_method = Api.linode_job_list 255 | primary_key = 'JobID' 256 | 257 | class Distribution(LinodeObject): 258 | fields = { 259 | 'id' : IntField('DistributionID'), 260 | 'label' : CharField('Label'), 261 | 'name' : CharField('Label'), 262 | 'min' : IntField('MinImageSize'), 263 | '64bit' : BoolField('Is64Bit'), 264 | 'created' : DateTimeField('CREATE_DT'), 265 | } 266 | 267 | list_method = Api.avail_distributions 268 | primary_key = 'DistributionID' 269 | 270 | class LinodeDisk(LinodeObject): 271 | fields = { 272 | 'id' : IntField('DiskID'), 273 | 'linode' : ForeignField(Linode), 274 | 'type' : ChoiceField('Type', choices=['ext3', 'swap', 'raw']), 275 | 'size' : IntField('Size'), 276 | 'name' : CharField('Label'), 277 | 'label' : CharField('Label'), 278 | 'status' : IntField('Status'), 279 | 'created' : DateTimeField('Create_DT'), 280 | 'updated' : DateTimeField('Update_DT'), 281 | 'readonly': BoolField('IsReadonly'), 282 | } 283 | 284 | update_method = Api.linode_disk_update 285 | create_method = Api.linode_disk_create 286 | primary_key = 'DiskID' 287 | list_method = Api.linode_disk_list 288 | 289 | def duplicate(self): 290 | ret = ActiveContext.linode_disk_duplicate(linodeid=self.linode.id, diskid=self.id) 291 | disk = LinodeDisk.get(linode=self.linode, id=ret['DiskID']) 292 | job = LinodeJob(linode=self.linode, id=ret['JobID']) 293 | return (disk, job) 294 | 295 | def resize(self, size): 296 | ret = ActiveContext.linode_disk_resize(linodeid=self.linode.id, diskid=self.id, size=size) 297 | return LinodeJob.get(linode=self.linode, id=ret['JobID']) 298 | 299 | def delete(self): 300 | ret = ActiveContext.linode_disk_delete(linodeid=self.linode.id, diskid=self.id) 301 | job = LinodeJob.get(linode=self.linode, id=ret['JobID']) 302 | self.cache_remove() 303 | return job 304 | 305 | @classmethod 306 | def create_from_distribution(self, linode, distribution, root_pass, label, size, ssh_key=None): 307 | l = ForeignField(Linode).to_linode(linode) 308 | d = ForeignField(Distribution).to_linode(distribution) 309 | ret = ActiveContext.linode_disk_createfromdistribution(linodeid=l, distributionid=d, 310 | rootpass=root_pass, label=label, size=size, rootsshkey=ssh_key) 311 | disk = self.get(id=ret['DiskID'], linode=linode) 312 | job = LinodeJob(id=ret['JobID'], linode=linode) 313 | return (disk, job) 314 | 315 | class Kernel(LinodeObject): 316 | fields = { 317 | 'id' : IntField('KernelID'), 318 | 'label' : CharField('Label'), 319 | 'name' : CharField('Label'), 320 | 'is_xen': BoolField('IsXen'), 321 | } 322 | 323 | list_method = Api.avail_kernels 324 | primary_key = 'KernelID' 325 | 326 | class LinodeConfig(LinodeObject): 327 | fields = { 328 | 'id' : IntField('ConfigID'), 329 | 'linode' : ForeignField(Linode), 330 | 'kernel' : ForeignField(Kernel), 331 | 'disklist' : ListField('DiskList', type=ForeignField(LinodeDisk)), 332 | 'name' : CharField('Label'), 333 | 'label' : CharField('Label'), 334 | 'comments' : CharField('Comments'), 335 | 'ram_limit' : IntField('RAMLimit'), 336 | 'root_device_num' : IntField('RootDeviceNum'), 337 | 'root_device_custom' : IntField('RootDeviceCustom'), 338 | 'root_device_readonly': BoolField('RootDeviceRO'), 339 | 'disable_updatedb' : BoolField('helper_disableUpdateDB'), 340 | 'helper_xen' : BoolField('helper_xen'), 341 | 'helper_depmod' : BoolField('helper_depmod'), 342 | } 343 | 344 | update_method = Api.linode_config_update 345 | create_method = Api.linode_config_create 346 | primary_key = 'ConfigID' 347 | list_method = Api.linode_config_list 348 | 349 | def delete(self): 350 | self.cache_remove() 351 | ActiveContext.linode_config_delete(linodeid=self.linode.id, configid=self.id) 352 | 353 | class LinodeIP(LinodeObject): 354 | fields = { 355 | 'id' : IntField('IPAddressID'), 356 | 'linode' : ForeignField(Linode), 357 | 'address' : CharField('IPADDRESS'), 358 | 'is_public' : BoolField('ISPUBLIC'), 359 | 'rdns' : CharField('RDNS_NAME'), 360 | } 361 | 362 | list_method = Api.linode_ip_list 363 | primary_key = 'IPAddressID' 364 | 365 | class Domain(LinodeObject): 366 | fields = { 367 | 'id' : IntField('DomainID'), 368 | 'domain' : CharField('Domain'), 369 | 'name' : CharField('Domain'), 370 | 'type' : ChoiceField('Type', choices=['master', 'slave']), 371 | 'soa_email' : CharField('SOA_Email'), 372 | 'refresh' : IntField('Refresh_sec'), 373 | 'retry' : IntField('Retry_sec'), 374 | 'expire' : IntField('Expire_sec'), 375 | 'ttl' : IntField('TTL_sec'), 376 | 'status' : ChoiceField('Status', choices=['0', '1', '2']), 377 | 'master_ips': ListField('master_ips', type=CharField('master_ips')), 378 | } 379 | 380 | update_method = Api.domain_update 381 | create_method = Api.domain_create 382 | primary_key = 'DomainID' 383 | list_method = Api.domain_list 384 | 385 | STATUS_OFF = 0 386 | STATUS_ON = 1 387 | STATUS_EDIT = 2 388 | 389 | def delete(self): 390 | self.cache_remove() 391 | ActiveContext.domain_delete(domainid=self.id) 392 | 393 | class Resource(LinodeObject): 394 | fields = { 395 | 'id' : IntField('ResourceID'), 396 | 'domain' : ForeignField(Domain), 397 | 'name' : CharField('Name'), 398 | 'type' : CharField('Type'), 399 | 'target' : CharField('Target'), 400 | 'priority' : IntField('Priority'), 401 | 'weight' : IntField('Weight'), 402 | 'port' : IntField('Port'), 403 | 'protocol' : CharField('Protocol'), 404 | 'ttl' : IntField('TTL_sec'), 405 | } 406 | 407 | update_method = Api.domain_resource_update 408 | create_method = Api.domain_resource_create 409 | primary_key = 'ResourceID' 410 | list_method = Api.domain_resource_list 411 | 412 | def delete(self): 413 | self.cache_remove() 414 | ActiveContext.domain_resource_delete(domainid=self.domain.id, resourceid=self.id) 415 | 416 | @classmethod 417 | def list_by_type(self, domain, only=None): 418 | resources = self.list(domain=domain) 419 | r_by_type = { 420 | 'A' : [], 421 | 'CNAME' : [], 422 | 'MX' : [], 423 | 'SRV' : [], 424 | 'TXT' : [], 425 | } 426 | 427 | for r in resources: r_by_type[r.type.upper()].append(r) 428 | 429 | if only: 430 | return r_by_type[only.upper()] 431 | else: 432 | return r_by_type 433 | 434 | def _iter_class(self, results): 435 | _id_cache[self] = {} 436 | results = LowerCaseDict(results) 437 | 438 | d = results['data'] 439 | for i in d: self(i).cache_add() 440 | 441 | def fill_cache(): 442 | _api.batching = True 443 | _api.linode_list() 444 | _api.avail_linodeplans() 445 | _api.avail_datacenters() 446 | _api.avail_distributions() 447 | _api.avail_kernels() 448 | _api.domain_list() 449 | ret = _api.batchFlush() 450 | 451 | for i,k in enumerate([Linode, LinodePlan, Datacenter, Distribution, Kernel, Domain]): 452 | _iter_class(k, ret[i]) 453 | 454 | for k in _id_cache[Linode].keys(): 455 | _api.linode_config_list(linodeid=k) 456 | _api.linode_disk_list(linodeid=k) 457 | 458 | for k in _id_cache[Domain].keys(): 459 | _api.domain_resource_list(domainid=k) 460 | 461 | ret = _api.batchFlush() 462 | 463 | for r in ret: 464 | r = LowerCaseDict(r) 465 | if r['action'] == 'linode.config.list': 466 | _iter_class(LinodeConfig, r) 467 | elif r['action'] == 'linode.disk.list': 468 | _iter_class(LinodeDisk, r) 469 | elif r['action'] == 'domain.resource.list': 470 | _iter_class(Resource, r) 471 | 472 | _api.batching = False 473 | 474 | def setup_logging(): 475 | logging.basicConfig(level=logging.DEBUG) 476 | -------------------------------------------------------------------------------- /linode/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim:ts=2:sw=2:expandtab 3 | """ 4 | A Python shell to interact with the Linode API 5 | 6 | Copyright (c) 2008 Timothy J Fontaine 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | """ 29 | 30 | import api 31 | import code 32 | import decimal 33 | import rlcompleter 34 | import readline 35 | import atexit 36 | import os 37 | try: 38 | import json 39 | except: 40 | import simplejson as json 41 | 42 | class DecimalEncoder(json.JSONEncoder): 43 | """Handle Decimal types when producing JSON. 44 | 45 | Hat tip: http://stackoverflow.com/questions/4019856/decimal-to-json 46 | """ 47 | def default(self, o): 48 | if isinstance(o, decimal.Decimal): 49 | return float(o) 50 | return json.JSONEncoder.default(self, o) 51 | 52 | class LinodeConsole(code.InteractiveConsole): 53 | def __init__(self, locals=None, filename="", 54 | histfile=os.path.expanduser("~/.linode-console-history")): 55 | code.InteractiveConsole.__init__(self) 56 | self.init_history(histfile) 57 | 58 | def init_history(self, histfile): 59 | if hasattr(readline, "read_history_file"): 60 | try: 61 | readline.read_history_file(histfile) 62 | except IOError: 63 | pass 64 | atexit.register(self.save_history, histfile) 65 | 66 | def save_history(self, histfile): 67 | readline.write_history_file(histfile) 68 | 69 | class LinodeComplete(rlcompleter.Completer): 70 | def complete(self, text, state): 71 | result = rlcompleter.Completer.complete(self, text, state) 72 | if result and result.find('__') > -1: 73 | result = '' 74 | return result 75 | 76 | 77 | if __name__ == "__main__": 78 | from getpass import getpass 79 | from os import environ 80 | import getopt, sys 81 | if 'LINODE_API_KEY' in environ: 82 | key = environ['LINODE_API_KEY'] 83 | else: 84 | key = getpass('Enter API Key: ') 85 | 86 | linode = api.Api(key) 87 | 88 | def usage(all=False): 89 | print('shell.py -- [--parameter1=value [--parameter2=value [...]]]') 90 | print('Valid Actions') 91 | for a in sorted(linode.valid_commands()): 92 | print('\t--'+a) 93 | if all: 94 | print('Valid Named Parameters') 95 | for a in sorted(linode.valid_params()): 96 | print('\t--'+a+'=') 97 | else: 98 | print('To see valid parameters use: --help --all') 99 | 100 | options = [] 101 | for arg in linode.valid_params(): 102 | options.append(arg+'=') 103 | 104 | for arg in linode.valid_commands(): 105 | options.append(arg) 106 | options.append('help') 107 | options.append('all') 108 | 109 | if len(sys.argv[1:]) > 0: 110 | try: 111 | optlist, args = getopt.getopt(sys.argv[1:], '', options) 112 | except getopt.GetoptError as err: 113 | print(str(err)) 114 | usage() 115 | sys.exit(2) 116 | 117 | command = optlist[0][0].replace('--', '') 118 | 119 | params = {} 120 | for param,value in optlist[1:]: 121 | params[param.replace('--', '')] = value 122 | 123 | if command == 'help' or 'help' in params: 124 | usage('all' in params) 125 | sys.exit(2) 126 | 127 | if hasattr(linode, command): 128 | func = getattr(linode, command) 129 | try: 130 | print(json.dumps(func(**params), indent=2, cls=DecimalEncoder)) 131 | except api.MissingRequiredArgument as mra: 132 | print('Missing option --%s' % mra.value.lower()) 133 | print('') 134 | usage() 135 | sys.exit(2) 136 | else: 137 | if not command == 'help': 138 | print('Invalid action '+optlist[0][0].lower()) 139 | 140 | usage() 141 | sys.exit(2) 142 | else: 143 | console = LinodeConsole() 144 | 145 | console.runcode('import readline,rlcompleter,api,shell,json') 146 | console.runcode('readline.parse_and_bind("tab: complete")') 147 | console.runcode('readline.set_completer(shell.LinodeComplete().complete)') 148 | console.runcode('def pp(text=None): print(json.dumps(text, indent=2, cls=shell.DecimalEncoder))') 149 | console.locals.update({'linode':linode}) 150 | console.interact() 151 | -------------------------------------------------------------------------------- /linode/tests.py: -------------------------------------------------------------------------------- 1 | import api 2 | import unittest 3 | import os 4 | from getpass import getpass 5 | 6 | class ApiTest(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.linode = api.Api(os.environ['LINODE_API_KEY']) 10 | 11 | def testAvailLinodeplans(self): 12 | available_plans = self.linode.avail_linodeplans() 13 | self.assertTrue(isinstance(available_plans, list)) 14 | 15 | def testEcho(self): 16 | test_parameters = {'FOO': 'bar', 'FIZZ': 'buzz'} 17 | response = self.linode.test_echo(**test_parameters) 18 | self.assertTrue('FOO' in response) 19 | self.assertTrue('FIZZ' in response) 20 | self.assertEqual(test_parameters['FOO'], response['FOO']) 21 | self.assertEqual(test_parameters['FIZZ'], response['FIZZ']) 22 | 23 | if __name__ == "__main__": 24 | if 'LINODE_API_KEY' not in os.environ: 25 | os.environ['LINODE_API_KEY'] = getpass('Enter API Key: ') 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name = "linode-python", 5 | version = "1.2", 6 | description = "Python bindings for Linode API", 7 | author = "TJ Fontaine", 8 | author_email = "tjfontaine@gmail.com", 9 | url = "https://github.com/tjfontaine/linode-python", 10 | packages = ['linode'], 11 | extras_require = { 12 | 'requests': ["requests"], 13 | }, 14 | ) 15 | --------------------------------------------------------------------------------