├── m ├── __init__.py ├── servers.py ├── config.py ├── orders.py ├── monitor.py ├── email.py ├── availability.py ├── print.py ├── api.py └── catalog.py ├── .gitignore ├── availability_monitor.py ├── README.md ├── conf.example.yaml ├── buy_ovh.py └── LICENSE /m/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ovh.conf 2 | /conf.py 3 | /conf.yaml 4 | __pycache__/ 5 | *.py[cod] 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /m/servers.py: -------------------------------------------------------------------------------- 1 | import m.api 2 | import m.print 3 | 4 | __all__ = ['servers_specs'] 5 | 6 | servers_specs_list = [] 7 | 8 | def servers_specs(printMessage=False): 9 | m.print.print_servers(m.api.get_servers_list(printMessage)) 10 | input("Press Enter.") 11 | -------------------------------------------------------------------------------- /m/config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import yaml 3 | 4 | __all__ = ['configFile'] 5 | 6 | # config path optionally given as argv 7 | config_path = sys.argv[1] if len(sys.argv) > 1 else 'conf.yaml' 8 | 9 | configFile = {} 10 | try: 11 | print(f"Loading config from {config_path}") 12 | configFile = yaml.safe_load(open(config_path, 'r')) 13 | except Exception as e: 14 | print("Error with config file") 15 | print(e) 16 | sys.exit("Bye now.") 17 | -------------------------------------------------------------------------------- /availability_monitor.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | import time 4 | 5 | import m.api 6 | import m.availability 7 | import m.monitor 8 | 9 | availabilities=[] 10 | previousAvailabilities=[] 11 | 12 | while True: 13 | try: 14 | if availabilities: 15 | previousAvailabilities = availabilities 16 | availabilities = m.availability.build_availability_dict(m.api.api_url("ovh-eu"), sys.argv[1:]) 17 | strChanged = m.monitor.avail_added_removed_Str(previousAvailabilities, availabilities) 18 | if strChanged: 19 | current_time = datetime.datetime.now() 20 | print(datetime.datetime.now(), " :") 21 | print(strChanged) 22 | time.sleep(30) 23 | except KeyboardInterrupt: 24 | break 25 | -------------------------------------------------------------------------------- /m/orders.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import m.api 4 | import m.print 5 | 6 | __all__ = ['unpaid_orders', 'undelivered_orders'] 7 | 8 | # ----------------- SHOW UNPAID ORDERS -------------------------------------------------------- 9 | def unpaid_orders(printMessage=False): 10 | # Get today's date 11 | today = datetime.now() 12 | tomorrow = today + timedelta(days=1) 13 | # Calculate the date 14 days ago 14 | date_14_days_ago = today - timedelta(days=14) 15 | 16 | unpaidOrderList = [] 17 | try: 18 | unpaidOrderList = m.api.get_orders_per_status(date_14_days_ago, tomorrow, ['notPaid'], printMessage) 19 | except KeyboardInterrupt: 20 | pass 21 | m.print.print_orders(unpaidOrderList, True) 22 | 23 | while True: 24 | sChoice = input("Which one? ") 25 | if not sChoice.isdigit() or int (sChoice) >= len(unpaidOrderList): 26 | break 27 | choice = int (sChoice) 28 | print ("URL: " + unpaidOrderList[choice]['url']) 29 | 30 | # ----------------- SHOW UNDELIVERED ORDERS -------------------------------------------------------- 31 | def undelivered_orders(printMessage=False): 32 | # Get today's date 33 | today = datetime.now() 34 | tomorrow = today + timedelta(days=1) 35 | # Calculate the date 14 days ago 36 | date_30_days_ago = today - timedelta(days=30) 37 | 38 | undeliveredOrderList = [] 39 | try: 40 | undeliveredOrderList = m.api.get_orders_per_status(date_30_days_ago, tomorrow, ['delivering'], printMessage) 41 | except KeyboardInterrupt: 42 | pass 43 | m.print.print_orders(undeliveredOrderList, True) 44 | 45 | while True: 46 | sChoice = input("Which one? ") 47 | if not sChoice.isdigit() or int (sChoice) >= len(undeliveredOrderList): 48 | break 49 | choice = int (sChoice) 50 | print ("URL: " + undeliveredOrderList[choice]['url']) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # buy_ovh.py 2 | Python script that uses the OVH API and their Python helper. See here: https://github.com/ovh/python-ovh 3 | 4 | The 'ovh' and 'PyYAML' modules must be installed. 5 | ``` 6 | pip install ovh 7 | ``` 8 | ``` 9 | pip install PyYAML 10 | ``` 11 | 12 | The main program is buy_ovh.py. There is also a small program availability_monitor which just does the availability monitoring. 13 | 14 | There is a conf file that you need to make, conf.yaml, where you can define stuff. There is an example file provided. The file explains all the parameters. 15 | 16 | In there you at least need the following for the connection to the API: endpoint, api key and secret. You can read about them at the python-ovh repo (above). A consumer key is also needed but the script will help you generate one if you have not got one. 17 | To know what parameter does what, read the code. 18 | 19 | It's recommended to have at least a filter on the server name (or plan code) otherwise the list will be huge. 20 | 21 | The colour coding is in the code. Red is unavailable. Green and yellow are available. Etc. 22 | 23 | Once you have chosen a server that happens to be available, press CTRL-C to stop the infinite loop (if you have defined 'loop' to true). 24 | Then you can chose which server you want, and if you want to generate the invoice or pay with your favourite method. 25 | 26 | You can also toggle the display of some stuff or the auto-loop, and change or empty the filters. 27 | 28 | The script can also show you a list of your unpaid orders and provide an URL if you want to pay for one. 29 | 30 | If you end up buying a 600€ server, it's not the script fault, it's yours, because this is just a random python script you found on the internet. 31 | 32 | I have only tested with OVH France. 33 | 34 | # Donations 35 | If you would like to make a small donation because this script helped you get the server of your dreams, feel free: https://paypal.me/fredo1664 36 | 37 | If it has to be in crypto, here's a Monero address: 8BTX8pJYmtf6STY2s7TdTT3NATocMpMReeV6DP5Vgnfx8MWBMXoTMypDjWv4Wf639habTNTED9bXSV9pYXs8s7BCHERFUxh 38 | -------------------------------------------------------------------------------- /m/monitor.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import m.availability 4 | import m.catalog 5 | 6 | __all__ = ['avail_added_removed_Str', 'avail_changed_Str', 'catalog_added_removed_Str'] 7 | 8 | # ---------------- EMAIL MONITOR AVAILAIBILITIES --------------------------------------- 9 | # - detect new servers appearing in availabilities (or leaving) 10 | # - monitor availability of some servers 11 | def avail_added_removed_Str(previousA, newA, preStr="", postStr=""): 12 | strToSend = "" 13 | # look for new FQN in availabilities (no filters) 14 | addedFqns, removedFqns = m.availability.added_removed(previousA, newA) 15 | if previousA and newA: 16 | for added in addedFqns: 17 | strToSend += preStr + "Added to availabilities: " + added + postStr + "\n" 18 | for removed in removedFqns: 19 | strToSend += preStr + "Removed from availabilities: " + removed + postStr + "\n" 20 | return strToSend 21 | 22 | def avail_changed_Str(previousA, newA, regex, preStr="", postStr=""): 23 | # look for availability change (unavailable <--> available) 24 | # for this there is a filter in order to not spam 25 | # the filter is on the FQN 26 | strToSend = "" 27 | availNow, availNotAnymore = m.availability.changed(previousA, newA, regex) 28 | for fqn in availNow: 29 | strToSend += preStr + "Available now: " + fqn + postStr + "\n" 30 | for fqn in availNotAnymore: 31 | strToSend += preStr + "No longer available: " + fqn + postStr + "\n" 32 | return strToSend 33 | 34 | # ---------------- EMAIL IF SOMETHING APPEARS IN THE CATALOG ----------------------------------- 35 | # The catalog is filtered (name and disk), so the new server must pass these filters 36 | def catalog_added_removed_Str(previousP, newP, preStr="", postStr=""): 37 | strChanged = "" 38 | addedFqns, removedFqns = m.catalog.added_removed(previousP, newP) 39 | for fqn in addedFqns: 40 | strChanged += preStr + "New to the catalog: " + fqn + postStr + "\n" 41 | for fqn in removedFqns: 42 | strChanged += preStr + "No longer in the catalog: " + fqn + postStr + "\n" 43 | return strChanged -------------------------------------------------------------------------------- /m/email.py: -------------------------------------------------------------------------------- 1 | import time 2 | import smtplib 3 | 4 | from email.mime.text import MIMEText 5 | from email.mime.multipart import MIMEMultipart 6 | 7 | import m.config 8 | 9 | __all__ = ['send_email', 'send_startup_email'] 10 | 11 | # from the conf file 12 | email_server_port = m.config.configFile['email_server_port'] if 'email_server_port' in m.config.configFile else 0 13 | email_server_name = m.config.configFile['email_server_name'] if 'email_server_name' in m.config.configFile else "" 14 | email_server_login = m.config.configFile['email_server_login'] if 'email_server_login' in m.config.configFile else "" 15 | email_server_password = m.config.configFile['email_server_password'] if 'email_server_password' in m.config.configFile else "" 16 | email_sender = m.config.configFile['email_sender'] if 'email_sender' in m.config.configFile else "" 17 | email_receiver = m.config.configFile['email_receiver'] if 'email_receiver' in m.config.configFile else "" 18 | 19 | # send an email 20 | def send_email(subject, text, warnUser=False): 21 | 22 | html = """\ 23 | 24 | 25 | """ + text + """\ 26 | 27 | 28 | """ 29 | try: 30 | # Create a multipart message and set headers 31 | message = MIMEMultipart() 32 | message["From"] = email_sender 33 | message["To"] = email_receiver 34 | message["Subject"] = subject 35 | 36 | # Attach the HTML part 37 | message.attach(MIMEText(html, "html")) 38 | 39 | # if not in infinite loop, warn the user 40 | if warnUser: 41 | print("Sending an email --> " + subject) 42 | # Send the email 43 | with smtplib.SMTP(email_server_name, email_server_port) as server: 44 | server.starttls() 45 | server.login(email_server_login, email_server_password) 46 | server.sendmail(email_sender, email_receiver, message.as_string()) 47 | except Exception as e: 48 | print("Failed to send an email.") 49 | print(e) 50 | time.sleep(2) 51 | 52 | def send_startup_email(): 53 | send_email("BUY_OVH: startup", "

BUY_OVH has started

") 54 | 55 | def send_auto_buy_email(string): 56 | send_email("BUY_OVH: autobuy", "

" + string + "

") 57 | -------------------------------------------------------------------------------- /m/availability.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | __all__ = ['unavailableList', 'unavailableAndUnknownList', 'added_removed', 'build_availability_dict', 'changed', 'look_up_avail'] 5 | 6 | # below lists are useful for 'if' statements 7 | unavailableList = ['comingSoon', 'unavailable'] 8 | unavailableAndUnknownList = ['comingSoon', 'unavailable', 'unknown'] 9 | 10 | # -------------- BUILD AVAILABILITY DICT ------------------------------------------------------------------------- 11 | def build_availability_dict(url, datacenters=[]): 12 | myAvail = {} 13 | if datacenters: 14 | response = requests.get(url + "dedicated/server/datacenter/availabilities?datacenters=" + ",".join(datacenters)) 15 | else: 16 | response = requests.get(url + "dedicated/server/datacenter/availabilities") 17 | for avail in response.json(): 18 | if 'fqn' in avail: 19 | myFqn = avail['fqn'] 20 | for da in avail['datacenters']: 21 | myLongFqn = myFqn + "." + da['datacenter'] 22 | myAvail[myLongFqn] = da['availability'] 23 | return myAvail 24 | 25 | # -------------- CHECK IF FQNS HAVE BEEN ADDED OR REMOVED ------------------------------------- 26 | def added_removed(previousA, newA): 27 | if previousA: 28 | return ([x for x in newA.keys() if x not in previousA.keys()], 29 | [x for x in previousA.keys() if x not in newA.keys()]) 30 | else: 31 | return ([],[]) 32 | 33 | # -------------- CHECK IF AVAILABILITY OF FQN HAS CHANGED ------------------------------------- 34 | def changed(previousA, newA, regex): 35 | # look for availability change (unavailable <--> available) 36 | # for this there is a filter in order to not spam 37 | # the filter is on the FQN 38 | availNow = [] 39 | availNotAnymore = [] 40 | if previousA: 41 | for fqn in newA: 42 | if bool(re.search(regex, fqn)): 43 | if (newA[fqn] not in unavailableAndUnknownList): 44 | # found an available server that matches the filter 45 | if (fqn not in previousA.keys() 46 | or previousA[fqn] in unavailableAndUnknownList): 47 | # its availability went from unavailable to available 48 | availNow.append(fqn) 49 | else: 50 | # found an unavailable server that matches the filter 51 | if (fqn in previousA.keys() 52 | and previousA[fqn] not in unavailableAndUnknownList): 53 | # its availability went from available to unavailable 54 | availNotAnymore.append(fqn) 55 | return (availNow, availNotAnymore) 56 | 57 | # ----------------- LOOK UP AVAILABILITIES ---------------------------------------------------- 58 | def look_up_avail(avail): 59 | 60 | sChoice = 'a' 61 | while sChoice: 62 | sChoice = input("FQN starts with: ") 63 | if sChoice: 64 | fqnsToShow = [] 65 | # size of column 66 | sizeCol = 0 67 | for eachFqn in avail.keys(): 68 | if eachFqn.startswith(sChoice): 69 | fqnsToShow.append(eachFqn) 70 | sizeCol = max(sizeCol, len(eachFqn)) 71 | for eachFqn in fqnsToShow: 72 | if eachFqn.startswith(sChoice): 73 | print(eachFqn.ljust(sizeCol) + " | " + avail[eachFqn]) -------------------------------------------------------------------------------- /conf.example.yaml: -------------------------------------------------------------------------------- 1 | # example configuration file for buy_ovh 2 | 3 | # API stuff 4 | # They are explained here: https://github.com/ovh/python-ovh 5 | APIEndpoint: ovh-eu 6 | APIKey: 123 7 | APISecret: 456 8 | #APIConsumerKey: 789 9 | 10 | # in which datacenters are we looking for? 11 | # this is a list 12 | datacenters: 13 | - gra 14 | - rbx 15 | - sbg 16 | - lon 17 | - fra 18 | - waw 19 | - bhs 20 | 21 | # the invoice name or plan code of the servers must match the regular expression: 22 | # example for all SYS from 2025, all KS-4, and KS-LE-B 23 | filterName: 25sys|24sk04|KS-LE-B 24 | 25 | # type of disks we want, usually among ssd,nvme,sa. 26 | # This is a regular expression 27 | # don't define it if you want all types 28 | filterDisk: ssd|nvme 29 | 30 | # memory filter, regular expression as well 31 | #filterMemory: 32g 32 | 33 | # max price for a server 34 | maxPrice: 100 35 | 36 | ovhSubsidiary: FR 37 | 38 | # add the VAT to the price? 39 | addVAT: True 40 | 41 | # show a helpful prompt after the list of server 42 | showPrompt: True 43 | # show the CPU type if available (unless showFqn=true) 44 | showCpu: False 45 | # show the servers even if they are unavailable 46 | showUnavailable: False 47 | # show the servers even if they have an unknown availability 48 | # (most probably there is a discrepancy between OVH's catalog and availabilities) 49 | showUnknown: False 50 | # show the bandwidth, including of the vRack if available 51 | # if this is False, only the price for the default bandwidth is displayed 52 | # (in case there are bandwidth options for the server) 53 | showBandwidth: False 54 | # print the FQN instead of spliting it 55 | # less readeable but up to you 56 | showFqn: False 57 | 58 | # define if you have a coupon 59 | # buying will fail with an incorrect coupon so be careful 60 | #coupon: MYCOUPON 61 | 62 | # if True, don't actually buy (for testing) 63 | # set to False if you want to actually buy the servers 64 | # Try the script once with fakeBuy but don't forget to set it to False after your test 65 | fakeBuy: True 66 | 67 | # Loop? 68 | # If True, the script refreshes the list of servers 69 | # every 'sleepsecs' seconds and does the monitoring (email sending). 70 | # You need to press CTRL-C to stop the loop and then you can choose. 71 | # If False, it displays the list of servers once and let you choose. 72 | # This is only the initial state, the user can always start the loop 73 | # by pressing 'L' 74 | loop: False 75 | 76 | # how many seconds before a refresh (if loop = True) 77 | sleepsecs: 20 78 | 79 | # email sending (email_on = False deactivate all emails) 80 | email_on: False 81 | 82 | # Send an email at startup (for test maybe) 83 | email_at_startup: False 84 | 85 | # Send email if something is added or removed on the 86 | # availability endpoint during a run 87 | email_added_removed: False 88 | 89 | # Monitor availability of servers whose FQN match the regular expression 90 | # This is a "super" FQN which has the datacenter at the end 91 | # Example: all KS-4 in GRA 92 | email_availability_monitor: 24sk40.*gra 93 | 94 | # Checks if any server was added to the catalog 95 | # These must pass the filters (name, disk and memory) 96 | email_catalog_monitor: False 97 | 98 | # For debug, send an email when an exception is raised during infinite loop 99 | email_exception: False 100 | 101 | # email server details 102 | email_server_port: 587 103 | email_server_name: "my.server.com" 104 | email_server_login: "login" 105 | email_server_password: "password" 106 | email_sender: "login@hello.com" 107 | email_receiver: "receiver@hello.com" 108 | -------------------------------------------------------------------------------- /m/print.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import m.availability 4 | 5 | __all__ = ['whichColor', 'print_plan_list', 'print_prompt', 'print_and_sleep', 'print_orders', 'print_servers'] 6 | 7 | # --- Coloring stuff ------------------------ 8 | class color: 9 | PURPLE = '\033[0;35;48m' 10 | CYAN = '\033[0;36;48m' 11 | BOLD = '\033[0;37;48m' 12 | BLUE = '\033[0;34;48m' 13 | GREEN = '\033[0;32;48m' 14 | YELLOW = '\033[0;33;48m' 15 | RED = '\033[0;31;48m' 16 | BLACK = '\033[0;30;48m' 17 | UNDERLINE = '\033[0;37;48m' 18 | END = '\033[0;37;0m' 19 | 20 | whichColor = { 'unknown' : color.CYAN, 21 | 'low' : color.YELLOW, 22 | 'high' : color.GREEN, 23 | 'unavailable' : color.RED, 24 | 'comingSoon' : color.BLUE, 25 | 'autobuy' : color.PURPLE 26 | } 27 | 28 | # ----------------- PRINT LIST OF SERVERS ----------------------------------------------------- 29 | def print_plan_list(plans, 30 | showCpu, showFqn, showBandwidth, 31 | showPrice, showFee, showTotalPrice): 32 | if not plans: 33 | print(whichColor['unavailable'] + "No availability." + color.END) 34 | sizeOfCol = { 35 | 'index' : 0, 36 | 'planCode' : 0, 37 | 'datacenter' : 0, 38 | 'model' : 0, 39 | 'cpu' : 0, 40 | 'fqn' : 0, 41 | 'memory' : 0, 42 | 'storage' : 0, 43 | 'bandwidth' : 0, 44 | 'vrack' : 0, 45 | 'price' : 0, 46 | 'fee' : 0, 47 | 'total' : 0 48 | } 49 | plansForDisplay = [] 50 | # determine what to print 51 | for plan in plans: 52 | if plan['vrack'] == 'none': 53 | vrack = 'none' 54 | else: 55 | vrack = plan['vrack'].split("-")[2] 56 | myPlanD = { 57 | 'index': str(plans.index(plan)), 58 | 'planCode': plan['planCode'], 59 | 'datacenter': plan['datacenter'], 60 | 'fqn': plan['fqn'], 61 | 'memory': plan['memory'].split("-")[1], 62 | 'storage': "-".join(plan['storage'].split("-")[1:-1]), 63 | 'bandwidth': plan['bandwidth'].split("-")[1], 64 | 'vrack': vrack, 65 | 'autobuy': plan['autobuy'], 66 | 'availability': plan['availability'], 67 | 'model': plan['model'], 68 | 'cpu': plan['cpu'], 69 | 'price': "{:.2f}".format(plan['price']), 70 | 'fee': "{:.2f}".format(plan['fee']), 71 | 'total': "{:.2f}".format(plan['fee']+plan['price']) 72 | } 73 | plansForDisplay.append(myPlanD) 74 | # update the max width of each column if needed 75 | for col in myPlanD.keys(): 76 | if col not in ['autobuy', 'availability']: 77 | sizeOfCol[col]=max(sizeOfCol[col], len(myPlanD[col])) 78 | 79 | # print the list 80 | for planD in plansForDisplay: 81 | # what colour? 82 | avail = planD['availability'] 83 | if avail in m.availability.unavailableList: 84 | printcolor = whichColor[avail] 85 | elif avail.endswith("low") or avail.endswith('H'): 86 | printcolor = whichColor['low'] 87 | elif avail.endswith("high"): 88 | printcolor = whichColor['high'] 89 | else: 90 | printcolor = whichColor['unknown'] 91 | # special colour for autobuy 92 | if planD['autobuy']: 93 | planColor = whichColor['autobuy'] 94 | else: 95 | planColor = printcolor 96 | # show CPU or not? 97 | if showCpu: 98 | modelStr = planD['model'].ljust(sizeOfCol['model']) + " | " + planD['cpu'].ljust(sizeOfCol['cpu']) 99 | else: 100 | modelStr = planD['model'].ljust(sizeOfCol['model']) 101 | # show FQN or split info into different columns? 102 | if showFqn: 103 | fqnStr = planColor + planD['fqn'].ljust(sizeOfCol['fqn']) + printcolor 104 | else: 105 | codeStr = planColor + planD['planCode'].ljust(sizeOfCol['planCode']) + printcolor 106 | fqnStr = codeStr + " | " + modelStr + " | " + \ 107 | planD['datacenter'].ljust(sizeOfCol['datacenter']) + " | " + \ 108 | planD['memory'].rjust(sizeOfCol['memory']) + " | " + \ 109 | planD['storage'].ljust(sizeOfCol['storage']) 110 | # show bandwidth and vrack? 111 | if showBandwidth: 112 | if planD['vrack'] == 'none': 113 | vRackStr = 'none' 114 | else: 115 | vRackStr = planD['vrack'].rjust(sizeOfCol['vrack']) 116 | bandwidthStr = planD['bandwidth'].rjust(sizeOfCol['bandwidth']) + " | " + vRackStr + " | " 117 | else: 118 | bandwidthStr = "" 119 | 120 | colStr = printcolor + planD['index'].rjust(sizeOfCol['index']) + " | " + \ 121 | fqnStr + " | " + bandwidthStr 122 | if showPrice: 123 | colStr = colStr + planD['price'].rjust(sizeOfCol['price']) + " | " 124 | if showFee: 125 | colStr = colStr + planD['fee'].rjust(sizeOfCol['fee']) + " | " 126 | if showTotalPrice: 127 | colStr = colStr + planD['total'].rjust(sizeOfCol['total']) + " | " 128 | colStr = colStr + color.END 129 | print(colStr) 130 | 131 | # ----------------- PRINT PROMPT -------------------------------------------------------------- 132 | def print_prompt(acceptable_dc, filterMemory, filterName, filterDisk, maxPrice, coupon): 133 | if maxPrice > 0: 134 | strPrice = "[" + str(maxPrice) + "]" 135 | else: 136 | strPrice = "" 137 | print("- DCs : [" + ",".join(acceptable_dc) 138 | + "] - Filters : [" + filterName 139 | + "][" + filterDisk 140 | + "][" + filterMemory 141 | + "]" + strPrice + " - Coupon : [" + coupon + "]") 142 | 143 | # ----------------- SLEEP x SECONDS ----------------------------------------------------------- 144 | def print_and_sleep(showMessage, sleepsecs): 145 | for i in range(sleepsecs,0,-1): 146 | if showMessage: 147 | print(f"- Refresh in {i}s. CTRL-C to stop and buy/quit.", end="\r", flush=True) 148 | time.sleep(1) 149 | 150 | # ----------------- PRINT AUTO BUY STATS ------------------------------------------------------- 151 | def print_auto_buy(autoBuyNum, autoBuyNumInit, autoOK, autoKO, autoFake): 152 | print("Auto buy left: " + str(autoBuyNum) + "/" + str(autoBuyNumInit) 153 | + " - OK: " + str(autoOK) + ", NOK: " + str(autoKO) + ", Fake: " + str(autoFake)) 154 | 155 | # ----------------- PRINT LIST OF ORDERS ------------------------------------------------------- 156 | def print_orders(orderList, printDate=False): 157 | for order in orderList: 158 | strOrder = str(orderList.index(order)).ljust(4) + "| " \ 159 | + order['description'].ljust(10) + "| " \ 160 | + order['location'] 161 | if printDate: 162 | strOrder = strOrder + "| " + order['date'] 163 | print (strOrder) 164 | 165 | # ------------------ PRINT SERVER SPECS ---------------------------------------------------------- 166 | def print_servers(server_list): 167 | for server in server_list: 168 | print ("NAME: " + server['name']) 169 | print ("DC: " + server['datacenter']) 170 | print ("CPU: " + server['cpu']) 171 | print ("RAM: " + server['memory']) 172 | print ("DISKS: " + " + ".join(server['disks'])) 173 | print () 174 | -------------------------------------------------------------------------------- /m/api.py: -------------------------------------------------------------------------------- 1 | import ovh 2 | import time 3 | from datetime import datetime, timezone 4 | 5 | __all__ = ['api_url','build_cart', 'checkout_cart', 'get_orders_per_status', 6 | 'get_consumer_key', 'get_servers_list', 'login', 'is_logged_in'] 7 | 8 | # --- Exceptions ---------------------------- 9 | class NotLoggedIn(Exception): 10 | def __init__(self, message): 11 | self.message = message 12 | super().__init__(self.message) 13 | 14 | # --- Variables ----------------------------- 15 | client = None 16 | 17 | # --- What is the URL of the API? -------------------------------------------------------------- 18 | def api_url(endpoint): 19 | if endpoint == 'ovh-ca': 20 | return "https://ca.api.ovh.com/v1/" 21 | elif endpoint == 'ovh-us': 22 | return "https://api.us.ovhcloud.com/v1/" 23 | else: 24 | return "https://eu.api.ovh.com/v1/" 25 | 26 | # ---------------- ARE WE LOGGED IN? ----------------------------------------------------------- 27 | def is_logged_in(): 28 | return client != None 29 | 30 | # ---------------- LOGIN TO THE API ------------------------------------------------------------ 31 | def login(endpoint, application_key, application_secret, consumer_key): 32 | global client 33 | try: 34 | tmpClient = ovh.Client(endpoint=endpoint, 35 | application_key=application_key, 36 | application_secret=application_secret, 37 | consumer_key=consumer_key) 38 | # if the API credential are incorrect, this will fail 39 | tmpClient.get('/me') 40 | client = tmpClient 41 | return True 42 | except Exception as e: 43 | return False 44 | 45 | # ---------------- GET A CONSUMER KEY ---------------------------------------------------------- 46 | def get_consumer_key(endpoint, application_key, application_secret): 47 | global client 48 | try: 49 | client = ovh.Client(endpoint=endpoint, 50 | application_key=application_key, 51 | application_secret=application_secret) 52 | 53 | ck = client.new_consumer_key_request() 54 | ck.add_recursive_rules(ovh.API_READ_WRITE, '/') 55 | validation = ck.request() 56 | 57 | print("Please visit %s to authenticate" % validation['validationUrl']) 58 | input("and press Enter to continue...") 59 | 60 | # this will fail if they did not authenticate 61 | client.get('/me') 62 | 63 | return validation['consumerKey'] 64 | except Exception as e: 65 | client = None 66 | return "nokey" 67 | 68 | # ---------------- BUILD THE CART -------------------------------------------------------------- 69 | def build_cart(plan, ovhSubsidiary, coupon, fake=False): 70 | if fake: 71 | print("Fake cart!") 72 | time.sleep(1) 73 | return 0 74 | elif client == None: 75 | raise NotLoggedIn("Need to be logged in to build the cart.") 76 | 77 | # make a cart 78 | cart = client.post("/order/cart", ovhSubsidiary=ovhSubsidiary) 79 | cartId = cart.get("cartId") 80 | client.post("/order/cart/{0}/assign".format(cartId)) 81 | # add the server 82 | result = client.post( 83 | f'/order/cart/{cart.get("cartId")}/eco', 84 | duration = "P1M", 85 | planCode = plan['planCode'], 86 | pricingMode = "default", 87 | quantity = 1 88 | ) 89 | itemId = result['itemId'] 90 | 91 | # add options 92 | result = client.post( 93 | f'/order/cart/{cartId}/eco/options', 94 | duration = "P1M", 95 | itemId = itemId, 96 | planCode = plan['memory'], 97 | pricingMode = "default", 98 | quantity = 1 99 | ) 100 | result = client.post( 101 | f'/order/cart/{cartId}/eco/options', 102 | itemId = itemId, 103 | duration = "P1M", 104 | planCode = plan['storage'], 105 | pricingMode = "default", 106 | quantity = 1 107 | ) 108 | result = client.post( 109 | f'/order/cart/{cartId}/eco/options', 110 | itemId = itemId, 111 | duration = "P1M", 112 | planCode = plan['bandwidth'], 113 | pricingMode = "default", 114 | quantity = 1 115 | ) 116 | if plan['vrack'] != 'none': 117 | result = client.post( 118 | f'/order/cart/{cartId}/eco/options', 119 | itemId = itemId, 120 | duration = "P1M", 121 | planCode = plan['vrack'], 122 | pricingMode = "default", 123 | quantity = 1 124 | ) 125 | 126 | # add configuration 127 | result = client.post( 128 | f'/order/cart/{cartId}/item/{itemId}/configuration', 129 | label = "dedicated_datacenter", 130 | value = plan['datacenter'] 131 | ) 132 | result = client.post( 133 | f'/order/cart/{cartId}/item/{itemId}/configuration', 134 | label = "dedicated_os", 135 | value = "none_64.en" 136 | ) 137 | if plan['datacenter'] in ["bhs", "syd", "sgp"]: 138 | myregion = "canada" 139 | else: 140 | myregion = "europe" 141 | result = client.post( 142 | f'/order/cart/{cartId}/item/{itemId}/configuration', 143 | label = "region", 144 | value = myregion 145 | ) 146 | 147 | # add coupon 148 | if coupon: 149 | result = client.post(f'/order/cart/{cartId}/coupon', 150 | label = "coupon", 151 | value = coupon) 152 | 153 | return cartId 154 | 155 | # ---------------- CHECKOUT THE CART --------------------------------------------------------- 156 | def checkout_cart(cartId, buyNow, fake=False): 157 | if fake: 158 | print("Fake buy! Now: " + str(buyNow)) 159 | time.sleep(2) 160 | return 161 | elif client == None: 162 | raise NotLoggedIn("Need to be logged in to check out the cart.") 163 | 164 | # this is it, we checkout the cart! 165 | result = client.post(f'/order/cart/{cartId}/checkout', 166 | autoPayWithPreferredPaymentMethod=buyNow, 167 | waiveRetractationPeriod=buyNow 168 | ) 169 | 170 | def get_orders_per_status(date_from, date_to, status_list, printMessage=False): 171 | if client == None: 172 | raise NotLoggedIn("Need to be logged in to get unpaid orders.") 173 | params = {} 174 | params['date.from'] = date_from.strftime('%Y-%m-%d') 175 | params['date.to'] = date_to.strftime('%Y-%m-%d') 176 | API_orders = client.get("/me/order/", **params) 177 | orderList = [] 178 | if printMessage: 179 | print("Building list of orders. Please wait.") 180 | for orderId in API_orders: 181 | if printMessage: 182 | print("(" + str(API_orders.index(orderId)+1) + "/" + str(len(API_orders)) + ")", end="\r", flush=True) 183 | orderStatus = client.get("/me/order/{0}/status/".format(orderId)) 184 | if orderStatus in status_list: 185 | details = client.get("/me/order/{0}/details/".format(orderId)) 186 | for detailId in details: 187 | orderDetail = client.get("/me/order/{0}/details/{1}".format(orderId, detailId)) 188 | if orderDetail['domain'] == '*001' and orderDetail['detailType'] == "DURATION": 189 | theOrder = client.get("/me/order/{0}/".format(orderId)) 190 | # check if the order has expired 191 | dt = datetime.fromisoformat(theOrder['expirationDate']) 192 | # Current time in UTC 193 | now = datetime.now(timezone.utc) 194 | # order awaiting delivery don't expire 195 | if now < dt or orderStatus == 'delivering': 196 | description = orderDetail['description'].split('|')[0].split(' ')[0] 197 | location = orderDetail['description'].split('-')[-2][-4:] 198 | orderURL = theOrder['url'] 199 | orderDate = theOrder['expirationDate'].split('T')[0] 200 | orderList.append({ 201 | 'orderId' : orderId, 202 | 'description' : description, 203 | 'location' : location, 204 | 'url' : orderURL, 205 | 'date' : orderDate}) 206 | break 207 | return orderList 208 | 209 | # ---------------- SERVERS ----------------------------------------------------------------------- 210 | def get_servers_list(printMessage=False): 211 | if client == None: 212 | raise NotLoggedIn("Need to be logged in to see the servers.") 213 | API_servers = client.get("/dedicated/server") 214 | servers_list = [] 215 | if printMessage: 216 | print("Building list of servers. Please wait.") 217 | for server_name in API_servers: 218 | if printMessage: 219 | print("(" + str(API_servers.index(server_name)+1) + "/" + str(len(API_servers)) + ")", end="\r", flush=True) 220 | # don't take expired or suspended ones 221 | server_info = client.get("/dedicated/server/"+server_name) 222 | if server_info['iam']['state']=="OK": 223 | server_hw = client.get("/dedicated/server/"+server_name+"/specifications/hardware") 224 | disk_groups = [x['description'] for x in server_hw['diskGroups']] 225 | servers_list.append({'id': server_name, 226 | 'name': server_info['iam']['displayName'], 227 | 'datacenter' : server_info['datacenter'], 228 | 'cpu' : server_hw['processorName'], 229 | 'memory': str(server_hw['memorySize']['value'])+" "+server_hw['memorySize']['unit'], 230 | 'disks': disk_groups 231 | }) 232 | return servers_list -------------------------------------------------------------------------------- /m/catalog.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | __all__ = ['added_removed', 'build_list'] 5 | 6 | # Here we fix errors in the catalog to match the FQN listed in the availabilities 7 | def fixMem(mem): 8 | fixedMem = mem 9 | # For 25rises011 and 021, OVH add "-rise-s" instead of the plancode at the end of the RAM 10 | # and in the availabilities there is an extra "-on-die-ecc-5200" 11 | if mem.endswith("-rise"): 12 | fixedMem = mem.removesuffix("-rise") + "-on-die-ecc-5200" 13 | elif mem.endswith("-16g"): 14 | # For KS-STOR and SYS-STOR they don't have the ECC part at the end of the mem in the catalog 15 | fixedMem = mem + "-ecc-2133" 16 | return fixedMem 17 | 18 | def fixSto(sto): 19 | fixedSto = sto 20 | # For SYS-01 with hybrid disks, the availabilities have 500nvme instead of 512nvme 21 | if sto.endswith("4000sa-2x512nvme") or sto.endswith("4000sa-1x512nvme"): 22 | fixedSto = sto.replace("512", "500") 23 | return fixedSto 24 | 25 | # -------------- EXTRACT THE PRICE INCLUDING PROMOTION ----------------------------------------------------------- 26 | def getPrice(price): 27 | myPrice = float(price['price'])/100000000 28 | allPromo = price['promotions'] 29 | if allPromo: 30 | allPercentPromo = [x for x in allPromo if x['type']=='percentage'] 31 | # take only the first one 32 | if allPercentPromo: 33 | myPromo = float(100-allPercentPromo[0]['value'])/100 34 | else: 35 | myPromo = 1 36 | else: 37 | myPromo = 1 38 | return myPrice * myPromo 39 | 40 | # -------------- BUILD LIST OF SERVERS --------------------------------------------------------------------------- 41 | def build_list(url, 42 | avail, ovhSubsidiary, 43 | filterName, filterDisk, filterMemory, acceptable_dc, maxPrice, 44 | addVAT, 45 | bandwidthAndVRack): 46 | response = requests.get(url + "order/catalog/public/eco?ovhSubsidiary=" + ovhSubsidiary) 47 | API_catalog = response.json() 48 | 49 | allPlans = API_catalog['plans'] 50 | myPlans = [] 51 | 52 | allAddons = API_catalog['addons'] 53 | 54 | try: 55 | vatRate = 1 + (API_catalog['locale']['taxRate']) / 100 56 | except: 57 | if addVAT: 58 | print("Could not read VAT from the catalog") 59 | vatRate = 1 60 | 61 | for plan in allPlans: 62 | planCode = plan['planCode'] 63 | invoiceNameSplit = plan['invoiceName'].split('|') 64 | model = invoiceNameSplit[0] 65 | if len(invoiceNameSplit) > 1: 66 | cpu = invoiceNameSplit[1][1:] 67 | # remove extra space at the end of the model name 68 | model = model[:-1] 69 | else: 70 | cpu = "unknown" 71 | # only consider plans passing the name filter, which is a regular expression 72 | # Either model (from invoice name) of plan code must match 73 | if not (bool(re.search(filterName, model)) 74 | or bool(re.search(filterName, plan['planCode']))): 75 | continue 76 | 77 | # find the price 78 | allPrices = plan['pricings'] 79 | # first pricing is the setup fee, second is the monthly price 80 | # (1 month commitment) 81 | if allPrices: 82 | planFee = getPrice(allPrices[0]) 83 | planPrice = getPrice(allPrices[1]) 84 | else: 85 | planFee = 0.0 86 | planPrice = 0.0 87 | 88 | allStorages = [] 89 | allMemories = [] 90 | allBandwidths = [] 91 | allVRack = [] 92 | 93 | # find mandatory addons 94 | for family in plan['addonFamilies']: 95 | if family['name'] == "storage": 96 | allStorages = family['addons'] 97 | elif family['name'] == "memory": 98 | allMemories = family['addons'] 99 | elif family['name'] == "bandwidth": 100 | allBandwidths = family['addons'] 101 | elif family['name'] == "vrack": 102 | allVRack = family['addons'] 103 | 104 | # vRack is not always present 105 | if not allVRack: 106 | allVRack = ['none'] 107 | 108 | allDatacenters = [] 109 | 110 | # same for datacenters 111 | for config in plan['configurations']: 112 | if config['name'] == "dedicated_datacenter": 113 | allDatacenters = config['values'] 114 | # filter and sort datacenters per acceptable_dc 115 | if acceptable_dc: 116 | filteredDatacenters = [x for x in allDatacenters if x in acceptable_dc] 117 | sortedDatacenters = sorted(filteredDatacenters, key=lambda x: acceptable_dc.index(x)) 118 | else: 119 | sortedDatacenters = allDatacenters 120 | 121 | # build a list of all possible combinations 122 | for da in sortedDatacenters: 123 | for me in allMemories: 124 | # the API adds the name of the plan at the end of the addons, drop it 125 | # (only for building the FQN) 126 | # for KS-LE-* they also add the v1 at the end which needs to go 127 | # Also there are sometimes differences between catalog and availabilities 128 | # fix these errors (only for building the FQN) 129 | if me.split("-")[-1] == "v1": 130 | shortme = fixMem("-".join(me.split("-")[:-2])) 131 | else: 132 | shortme = fixMem("-".join(me.split("-")[:-1])) 133 | # apply the memory filter 134 | if not bool(re.search(filterMemory,shortme)): 135 | continue 136 | for st in allStorages: 137 | if st.split("-")[-1] == "v1": 138 | shortst = fixSto("-".join(st.split("-")[:-2])) 139 | else: 140 | shortst = fixSto("-".join(st.split("-")[:-1])) 141 | # apply the disk filter 142 | if not bool(re.search(filterDisk,shortst)): 143 | continue 144 | for ba in allBandwidths: 145 | for vr in allVRack: 146 | # each config may have a different price within the same plan 147 | thisPrice = planPrice 148 | thisFee = planFee 149 | # try to find out the full price 150 | try: 151 | storagePlan = [x for x in allAddons if (x['planCode'] == st)] 152 | thisFee = thisFee + getPrice(storagePlan[0]['pricings'][0]) 153 | thisPrice = thisPrice + getPrice(storagePlan[0]['pricings'][1]) 154 | except Exception as e: 155 | print(e) 156 | try: 157 | memoryPlan = [x for x in allAddons if (x['planCode'] == me)] 158 | thisFee = thisFee + getPrice(memoryPlan[0]['pricings'][0]) 159 | thisPrice = thisPrice + getPrice(memoryPlan[0]['pricings'][1]) 160 | except Exception as e: 161 | print(e) 162 | try: 163 | bandwidthPlan = [x for x in allAddons if (x['planCode'] == ba)] 164 | bandwidthPrice = getPrice(bandwidthPlan[0]['pricings'][1]) 165 | # if showBandwidth is false, drop the plans with a bandwidth that costs money 166 | if not bandwidthAndVRack and bandwidthPrice > 0.0: 167 | continue 168 | # not sure if there is setup fee for the bandwidth? 169 | thisPrice = thisPrice + bandwidthPrice 170 | except Exception as e: 171 | print(e) 172 | if vr != 'none': 173 | try: 174 | vRackPlan = [x for x in allAddons if (x['planCode'] == vr)] 175 | vRackPrice = getPrice(vRackPlan[0]['pricings'][2]) 176 | # if showBandwidth is false, drop the plans with a vRack that costs money 177 | if not bandwidthAndVRack and vRackPrice > 0.0: 178 | continue 179 | # not sure if there is setup fee for the vRack? 180 | thisPrice = thisPrice + vRackPrice 181 | except Exception as e: 182 | print(e) 183 | if addVAT: 184 | # apply the VAT to the price 185 | thisFee = round(thisFee * vatRate, 2) 186 | thisPrice = round(thisPrice * vatRate, 2) 187 | # apply the max price filter if different from 0 188 | if maxPrice > 0 and thisPrice > maxPrice: 189 | continue 190 | myFqn = planCode + "." + shortme + "." + shortst + "." + da 191 | if myFqn in avail: 192 | myavailability = avail[myFqn] 193 | else: 194 | myavailability = 'unknown' 195 | # Add the plan to the list 196 | myPlans.append( 197 | { 'planCode' : planCode, 198 | 'model' : model, 199 | 'cpu' : cpu, 200 | 'datacenter' : da, 201 | 'storage' : st, 202 | 'memory' : me, 203 | 'bandwidth' : ba, 204 | 'vrack' : vr, 205 | 'fqn' : myFqn, # for auto buy 206 | 'price' : thisPrice, 207 | 'fee' : thisFee, 208 | 'availability' : myavailability 209 | }) 210 | return sorted(myPlans, key=lambda x: x['planCode']) 211 | 212 | # -------------- ADD AUTO BUY INFO TO PLAN LIST --------------------------------------------- 213 | def add_auto_buy(plans, autoBuyRE, autoBuyMaxPrice): 214 | for plan in plans: 215 | plan['autobuy'] = (autoBuyRE and 216 | (bool(re.search(autoBuyRE, plan['fqn'])) or bool(re.search(autoBuyRE, plan['model']))) 217 | and (autoBuyMaxPrice == 0 or plan['price'] <= autoBuyMaxPrice)) 218 | 219 | # -------------- CHECK IF A SERVER WAS ADDED OR REMOVED ------------------------------------- 220 | def added_removed(previousP, newP): 221 | addedFqns = [] 222 | removedFqns = [] 223 | if previousP: 224 | previousFqns = [x['fqn'] for x in previousP] 225 | newFqns = [x['fqn'] for x in newP] 226 | addedFqns = [ x for x in newFqns if x not in previousFqns] 227 | removedFqns = [ x for x in previousFqns if x not in newFqns] 228 | return (addedFqns, removedFqns) 229 | -------------------------------------------------------------------------------- /buy_ovh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | 6 | # modules 7 | import m.api 8 | import m.availability 9 | import m.catalog 10 | import m.email 11 | import m.monitor 12 | import m.orders 13 | import m.print 14 | import m.servers 15 | 16 | from m.config import configFile 17 | 18 | # ----------------- GLOBAL VARIABLES ---------------------------------------------------------- 19 | 20 | def loadConfigMain(cf): 21 | global acceptable_dc, filterName, filterDisk, filterMemory, maxPrice, addVAT, APIEndpoint, ovhSubsidiary, \ 22 | loop, sleepsecs, showPrompt, showCpu, showFqn, showUnavailable, showUnknown, \ 23 | showBandwidth, fakeBuy, coupon, \ 24 | showPrice, showFee, showTotalPrice 25 | acceptable_dc = cf['datacenters'] if 'datacenters' in cf else acceptable_dc 26 | filterName = cf['filterName'] if 'filterName' in cf else filterName 27 | filterDisk = cf['filterDisk'] if 'filterDisk' in cf else filterDisk 28 | filterMemory = cf['filterMemory'] if 'filterMemory' in cf else filterMemory 29 | maxPrice = cf['maxPrice'] if 'maxPrice' in cf else maxPrice 30 | addVAT = cf['addVAT'] if 'addVAT' in cf else addVAT 31 | ovhSubsidiary = cf['ovhSubsidiary'] if 'ovhSubsidiary' in cf else ovhSubsidiary 32 | APIEndpoint = cf['APIEndpoint'] if 'APIEndpoint' in cf else APIEndpoint 33 | loop = cf['loop'] if 'loop' in cf else loop 34 | sleepsecs = cf['sleepsecs'] if 'sleepsecs' in cf else sleepsecs 35 | showPrompt = cf['showPrompt'] if 'showPrompt' in cf else showPrompt 36 | showCpu = cf['showCpu'] if 'showCpu' in cf else showCpu 37 | showFqn = cf['showFqn'] if 'showFqn' in cf else showFqn 38 | showUnavailable = cf['showUnavailable'] if 'showUnavailable' in cf else showUnavailable 39 | showUnknown = cf['showUnknown'] if 'showUnknown' in cf else showUnknown 40 | showBandwidth = cf['showBandwidth'] if 'showBandwidth' in cf else showBandwidth 41 | showPrice = cf['showPrice'] if 'showPrice' in cf else showPrice 42 | showFee = cf['showFee'] if 'showFee' in cf else showFee 43 | showTotalPrice = cf['showTotalPrice'] if 'showTotalPrice' in cf else showTotalPrice 44 | fakeBuy = cf['fakeBuy'] if 'fakeBuy' in cf else fakeBuy 45 | coupon = cf['coupon'] if 'coupon' in cf else coupon 46 | 47 | def loadConfigEmail(cf): 48 | global email_on, email_at_startup, email_auto_buy, email_added_removed, \ 49 | email_availability_monitor, email_catalog_monitor, email_exception 50 | email_on = cf['email_on'] if 'email_on' in cf else email_on 51 | email_at_startup = cf['email_at_startup'] if 'email_at_startup' in cf and email_on else email_at_startup 52 | email_auto_buy = cf['email_auto_buy'] if 'email_auto_buy' in cf and email_on else email_auto_buy 53 | email_added_removed = cf['email_added_removed'] if 'email_added_removed' in cf and email_on else email_added_removed 54 | email_availability_monitor = cf['email_availability_monitor'] if 'email_availability_monitor' in cf and email_on else email_availability_monitor 55 | email_catalog_monitor = cf['email_catalog_monitor'] if 'email_catalog_monitor' in cf and email_on else email_catalog_monitor 56 | email_exception = cf['email_exception'] if 'email_exception' in cf and email_on else email_exception 57 | 58 | def loadConfigAutoBuy(cf): 59 | global autoBuyRE, autoBuyNum, autoBuyMaxPrice, autoBuyInvoicesNum, autoBuyUnknown, \ 60 | autoBuyNumInit, autoOK, autoKO, autoFake 61 | autoBuyRE = cf['auto_buy'] if 'auto_buy' in cf else autoBuyRE 62 | autoBuyNum = cf['auto_buy_num'] if 'auto_buy_num' in cf else autoBuyNum 63 | autoBuyMaxPrice = cf['auto_buy_max_price'] if 'auto_buy_max_price' in cf else autoBuyMaxPrice 64 | autoBuyInvoicesNum = cf['auto_buy_num_invoices'] if 'auto_buy_num_invoices' in cf else autoBuyInvoicesNum 65 | autoBuyUnknown = cf['auto_buy_unknown'] if 'auto_buy_unknown' in cf else autoBuyUnknown 66 | if autoBuyNum == 0: 67 | autoBuyRE = "" 68 | autoBuyNumInit = autoBuyNum 69 | autoOK = 0 70 | autoKO = 0 71 | autoFake = 0 72 | 73 | acceptable_dc = [] 74 | filterName = "" 75 | filterDisk = "" 76 | filterMemory = "" 77 | maxPrice = 0 78 | addVAT = False 79 | ovhSubsidiary = "FR" 80 | APIEndpoint = "ovh-eu" 81 | loop = False 82 | sleepsecs = 60 83 | showPrompt = True 84 | showCpu = True 85 | showFqn = False 86 | showUnavailable = True 87 | showUnknown = True 88 | showBandwidth = True 89 | showPrice = True 90 | showFee = False 91 | showTotalPrice = False 92 | fakeBuy = True 93 | coupon = '' 94 | 95 | loadConfigMain(configFile) 96 | 97 | email_on = False 98 | email_at_startup = False 99 | email_auto_buy = False 100 | email_added_removed = False 101 | email_availability_monitor = "" 102 | email_catalog_monitor = False 103 | email_exception = False 104 | loadConfigEmail(configFile) 105 | 106 | # Auto Buy 107 | autoBuyRE = "" 108 | autoBuyNum = 1 109 | autoBuyMaxPrice = 0 110 | autoBuyInvoicesNum = 0 111 | autoBuyNumInit = 0 112 | autoBuyUnknown = False 113 | # counters to display how auto buy are doing 114 | autoOK = 0 115 | autoKO = 0 116 | autoFake = 0 117 | loadConfigAutoBuy(configFile) 118 | 119 | # ----------------- CONNECT IF INFO IN CONF FILE ---------------------------------------------- 120 | if ('APIKey' in m.config.configFile and 121 | 'APISecret' in m.config.configFile): 122 | # if the customer key is there too, we can connect 123 | if 'APIConsumerKey' in m.config.configFile: 124 | m.api.login(APIEndpoint, 125 | m.config.configFile['APIKey'], 126 | m.config.configFile['APISecret'], 127 | m.config.configFile['APIConsumerKey']) 128 | else: 129 | ck = m.api.get_consumer_key(APIEndpoint, 130 | m.config.configFile['APIKey'], 131 | m.config.configFile['APISecret']) 132 | if ck != "nokey": 133 | print("To add the generated consumer key to your conf.yaml file:") 134 | print("APIConsumerKey: " + ck) 135 | else: 136 | print("Failed to get a consumer key, did you authenticate?") 137 | input("Press Enter to continue...") 138 | 139 | # ----------------- DISPLAY HELP -------------------------------------------------------------- 140 | def showHelp(): 141 | print("") 142 | print("Colour coding") 143 | print("-------------") 144 | print(m.print.whichColor['high'] + "Available HIGH") 145 | print(m.print.whichColor['low'] + "Available LOW") 146 | print(m.print.whichColor['unavailable'] + "Unavailable") 147 | print(m.print.whichColor['comingSoon'] + "Coming Soon") 148 | print(m.print.whichColor['unknown'] + "Availability unknown" + m.print.color.END) 149 | print("") 150 | print("Infinite Loop") 151 | print("-------------") 152 | print("When the loop is ON, the script updates the catalog and availabilities every " + str(sleepsecs) + "s.") 153 | print("You need to press CTRL-C to stop the loop and interact with the script.") 154 | print("") 155 | print("Toggles") 156 | print("-------") 157 | print(" B - show Bandwidth and vRack options ON/OFF") 158 | print(" C - show CPU type ON/OFF") 159 | print(" F - show FQN instead of server details ON/OFF") 160 | print(" P - show helpful prompt ON/OFF") 161 | print(" PP - show the monthly price ON/OFF") 162 | print(" PF - show the installation fee ON/OFF") 163 | print(" PT - show the total price ON/OFF") 164 | print(" U - show Unavailable servers ON/OFF") 165 | print(" UK - show servers with Unknown availability ON/OFF") 166 | print(" T - add Tax (VAT) to the price ON/OFF") 167 | print(" $ - fake buy ON/OFF") 168 | print("") 169 | print("Filters") 170 | print("-------") 171 | print(" FD - re-enter the Disk filter (sa, nvme, ssd)") 172 | print(" FM - re-enter the Memory filter (ex: 32g)") 173 | print(" FN - re-enter the Name filter (invoice name or plan code)") 174 | print(" FP - set maximum price") 175 | print("") 176 | print("Commands") 177 | print("--------") 178 | print(" D - show your undelivered orders and a link to see your bill for one") 179 | print(" K - enter a coupon (buying will fail if coupon is invalid)") 180 | print(" L - (re)start the infinite loop, activating monitoring if configured") 181 | print(" O - show your unpaid orders and a link to pay for one") 182 | print(" R - reload the configuration file") 183 | print(" S - print a list of your servers with some specs") 184 | print(" V - look up availabilities for a specific FQN") 185 | print("") 186 | print("Buying") 187 | print("------") 188 | print("Enter the server number in the list to either get an invoice or buy it straight away.") 189 | print(" Example :> 0") 190 | print("Start with ! to buy it now, ? for invoice.") 191 | print(" Example :> ?1") 192 | print("Add * followed by a number to buy multiple time") 193 | print("(this creates as many orders, each of them for one server)") 194 | print(" Example :> !3*4") 195 | print("") 196 | print("It is possible to enter more than one command at a time.") 197 | print("For example, to deactivate fake buy, buy 2 servers number 6 and get one invoice, re-activate fake buy and then restart the loop:") 198 | print(" > $ !6*2 ?6 $ l") 199 | print("") 200 | dummy=input("Press ENTER.") 201 | 202 | # ----------------- BUY SERVER ---------------------------------------------------------------- 203 | def buyServer(plan, buyNow, autoMode): 204 | global autoFake, autoOK, autoKO 205 | if autoMode: 206 | strAuto = " -Auto Mode-" 207 | else: 208 | strAuto = "" 209 | if buyNow: 210 | strBuyNow = "buy now a " 211 | else: 212 | strBuyNow = "get an invoice for a " 213 | strBuy = strBuyNow + plan['model'] + " in " + plan['datacenter'] + "." 214 | print("Let's " + strBuy + strAuto) 215 | try: 216 | m.api.checkout_cart(m.api.build_cart(plan, ovhSubsidiary, coupon, fakeBuy), buyNow, fakeBuy) 217 | if autoMode: 218 | if fakeBuy: 219 | autoFake += 1 220 | else: 221 | autoOK += 1 222 | if email_auto_buy and loop: 223 | m.email.send_auto_buy_email("SUCCESS: " + strBuy) 224 | except Exception as e: 225 | print("Not today.") 226 | print(e) 227 | if autoMode: 228 | autoKO += 1 229 | if email_auto_buy and loop: 230 | m.email.send_auto_buy_email("FAILED: " + strBuy) 231 | time.sleep(3) 232 | 233 | # ------------------ TOOL --------------------------------------------------------------------- 234 | # when ordering servers, the user can type something like "!0*3" 235 | # "*3" means repeat 3 times 236 | # this function expand these, so "!2*3" becomes "!2 !2 !2" 237 | # if no multiplier is specified, it means 1 238 | def expandMulti(line): 239 | pattern = r'(^|\s)([?!]?\d+)\*(\d+)' 240 | 241 | def replacer(match): 242 | first, word, count = match.groups() 243 | return first + ' '.join([word] * int(count)) 244 | 245 | return re.sub(pattern, replacer, line) 246 | # ----------------- MAIN PROGRAM -------------------------------------------------------------- 247 | 248 | # send email at startup 249 | if email_at_startup: 250 | m.email.send_startup_email() 251 | 252 | availabilities = {} 253 | # previous list of availabilities so we can send email if something pops up 254 | previousAvailabilities = {} 255 | 256 | # Plans which pass the filters (name + disk) 257 | plans = [] 258 | # previous plans 259 | previousPlans = [] 260 | # Unavailable servers can be hidden (see conf file), 261 | # so we need a list of non hidden plans for display and order 262 | displayedPlans = [] 263 | 264 | # do the catalog monitoring only if filters have not changed 265 | filtersChanged = False 266 | 267 | # loop until the user wants out 268 | while True: 269 | 270 | try: 271 | while True: 272 | try: 273 | os.system('cls' if os.name == 'nt' else 'clear') 274 | if availabilities: 275 | previousAvailabilities = availabilities 276 | previousPlans = plans 277 | availabilities = m.availability.build_availability_dict(m.api.api_url(APIEndpoint),acceptable_dc) 278 | plans = m.catalog.build_list(m.api.api_url(APIEndpoint), 279 | availabilities, 280 | ovhSubsidiary, 281 | filterName, filterDisk, filterMemory, acceptable_dc, maxPrice, 282 | addVAT, 283 | showBandwidth) 284 | m.catalog.add_auto_buy(plans, autoBuyRE, autoBuyMaxPrice) 285 | displayedPlans = [ x for x in plans \ 286 | if (x['availability'] not in m.availability.unavailableAndUnknownList or 287 | (x['availability'] in m.availability.unavailableList and showUnavailable) or 288 | (x['availability'] == 'unknown' and showUnknown) or 289 | x['autobuy'])] 290 | m.print.print_plan_list(displayedPlans, showCpu, showFqn, showBandwidth, 291 | showPrice, showFee, showTotalPrice) 292 | if fakeBuy: 293 | print("- Fake Buy ON") 294 | if not m.api.is_logged_in(): 295 | print("- Not logged in") 296 | foundAutoBuyServer = False 297 | if autoBuyRE: 298 | for plan in plans: 299 | if (plan['autobuy'] and 300 | autoBuyNum > 0 and 301 | (plan['availability'] not in m.availability.unavailableAndUnknownList or 302 | (plan['availability'] == 'unknown' and autoBuyUnknown)) 303 | ): 304 | # auto buy 305 | foundAutoBuyServer = True 306 | # The last x are invoices (rather than direct buy) if a number 307 | # of invoices is defined in the config file 308 | autoBuyInvoice = autoBuyNum <= autoBuyInvoicesNum 309 | buyServer(plan, not autoBuyInvoice, True) 310 | autoBuyNum -= 1 311 | if autoBuyNum < 1: 312 | autoBuyRE = "" 313 | break 314 | # availability and catalog monitor if configured 315 | strAvailMonitor = "" 316 | if email_added_removed: 317 | strAvailMonitor = m.monitor.avail_added_removed_Str(previousAvailabilities, availabilities, "

", "

") 318 | if email_availability_monitor: 319 | strAvailMonitor = strAvailMonitor + \ 320 | m.monitor.avail_changed_Str(previousAvailabilities, 321 | availabilities, 322 | email_availability_monitor, 323 | "

", "

") 324 | if strAvailMonitor: 325 | m.email.send_email("BUY_OVH: availabilities", strAvailMonitor, not loop) 326 | # Don't do the catalog monitoring if the user has just changed the filters 327 | if not filtersChanged: 328 | strCatalogMonitor = m.monitor.catalog_added_removed_Str(previousPlans, plans, "

", "

") 329 | if strCatalogMonitor: 330 | m.email.send_email("BUY_OVH: catalog", strCatalogMonitor, not loop) 331 | else: 332 | filtersChanged = False 333 | # if the conf says no loop, jump to the menu 334 | if not loop: 335 | if showPrompt: 336 | m.print.print_prompt(acceptable_dc, filterMemory, filterName, filterDisk, maxPrice, coupon) 337 | # if there has been at least one auto buy, show counters 338 | if autoBuyNumInit > 0 and autoBuyNum < autoBuyNumInit: 339 | m.print.print_auto_buy(autoBuyNum, autoBuyNumInit, 340 | autoOK, autoKO, autoFake) 341 | break 342 | if not foundAutoBuyServer: 343 | if showPrompt: 344 | m.print.print_prompt(acceptable_dc, filterMemory, filterName, filterDisk, maxPrice, coupon) 345 | if autoBuyNumInit > 0 and autoBuyNum < autoBuyNumInit: 346 | m.print.print_auto_buy(autoBuyNum, autoBuyNumInit, 347 | autoOK, autoKO, autoFake) 348 | m.print.print_and_sleep(showPrompt, sleepsecs) 349 | except KeyboardInterrupt: 350 | raise 351 | except Exception as e: 352 | print("Exception!") 353 | print(e) 354 | if loop and email_exception: 355 | m.email.send_email("BUY_OVH: Exception",str(e)) 356 | print("Wait " + str(sleepsecs) + "s before retry.") 357 | time.sleep(sleepsecs) 358 | except KeyboardInterrupt: 359 | pass 360 | 361 | print("") 362 | # stop the infinite loop, the user must press L to restart it 363 | loop = False 364 | allChoices = input("(H for Help)> ") 365 | # The user can specify to buy a server multiple times 366 | # "2*5" means buy server 2, 5 times 367 | # "2" and "2*1" mean the same thing 368 | # "!2*3 ?2*10" works too (see below for ! and ?) 369 | allChoicesExpanded = expandMulti(allChoices) 370 | listChoices = allChoicesExpanded.split(' ') 371 | for sChoice in listChoices: 372 | # when buying, the user can specify if they want an invoice or buy now, by starting with ? or ! 373 | # example: ?2 means an invoice for server two 374 | # !4 means buy server 4 now 375 | # 3 means I want server 3 but ask me if I want an invoice or to buy now 376 | if sChoice.startswith('?'): 377 | # invoice, no need to ask 378 | whattodo = 'i' 379 | sChoice = sChoice[1:] 380 | elif sChoice.startswith('!'): 381 | # buy now, no need to ask 382 | whattodo = 'n' 383 | sChoice = sChoice[1:] 384 | else: 385 | # if it's a server number, we'll ask if use wants an invoice or buy now 386 | whattodo = 'a' 387 | # if the user entered a number, it's a server number so let's buy it or get an invoice 388 | if sChoice.isdigit(): 389 | choice = int (sChoice) 390 | if choice >= len(displayedPlans): 391 | sys.exit("You had one job.") 392 | if whattodo == 'a': 393 | print(displayedPlans[choice]['model']) 394 | whattodo = input("Last chance : Make an invoice = I , Buy now = N , other = out : ").lower() 395 | if whattodo == 'i': 396 | mybool = False 397 | elif whattodo == 'n': 398 | mybool = True 399 | else: 400 | continue 401 | buyServer(displayedPlans[choice], mybool, False) 402 | # not a number means command 403 | # the '?', '!', and '*' have no effect here 404 | elif sChoice.lower() == 'fd': 405 | print("Current: " + filterDisk) 406 | filterDisk = input("New filter: ") 407 | filtersChanged = True 408 | elif sChoice.lower() == 'fm': 409 | print("Current: " + filterMemory) 410 | filterMemory = input("New filter: ") 411 | filtersChanged = True 412 | elif sChoice.lower() == 'fn': 413 | print("Current: " + filterName) 414 | filterName = input("New filter: ") 415 | filtersChanged = True 416 | elif sChoice.lower() == 'fp': 417 | if maxPrice > 0: 418 | print("Current:" + str(maxPrice)) 419 | else: 420 | print("Current: None") 421 | tmpMaxPrice = input("New Max Price: ") 422 | if tmpMaxPrice == "": 423 | maxPrice = 0 424 | else: 425 | maxPrice = float(tmpMaxPrice) 426 | filtersChanged = True 427 | elif sChoice.lower() == 'k': 428 | print("Current: " + coupon) 429 | coupon = input("Enter Coupon: ") 430 | elif sChoice.lower() == 'uk': 431 | showUnknown = not showUnknown 432 | elif sChoice.lower() == 'u': 433 | showUnavailable = not showUnavailable 434 | elif sChoice.lower() == 'p': 435 | showPrompt = not showPrompt 436 | elif sChoice.lower() == 'pp': 437 | showPrice = not showPrice 438 | elif sChoice.lower() == 'pf': 439 | showFee = not showFee 440 | elif sChoice.lower() == 'pt': 441 | showTotalPrice = not showTotalPrice 442 | elif sChoice.lower() == 'c': 443 | showCpu = not showCpu 444 | elif sChoice.lower() == 'f': 445 | showFqn = not showFqn 446 | elif sChoice.lower() == 'b': 447 | showBandwidth = not showBandwidth 448 | filtersChanged = True 449 | elif sChoice == '$': 450 | fakeBuy = not fakeBuy 451 | elif sChoice.lower() == 'l': 452 | loop = True 453 | elif sChoice.lower() == 'o': 454 | m.orders.unpaid_orders(True) 455 | elif sChoice.lower() == 'd': 456 | m.orders.undelivered_orders(True) 457 | elif sChoice.lower() == 'r': 458 | # reload conf 459 | loadConfigMain(configFile) 460 | filtersChanged = True 461 | elif sChoice.lower() == 'rr': 462 | # reload conf including autobuy 463 | loadConfigMain(configFile) 464 | loadConfigAutoBuy(configFile) 465 | filtersChanged = True 466 | elif sChoice.lower() == 's': 467 | m.servers.servers_specs(True) 468 | elif sChoice.lower() == 't': 469 | addVAT = not addVAT 470 | # VAT increases the price which could no longer pass the max price filter 471 | # so a server could "disappear" or "appear" in the catalog 472 | # triggering the catalog monitor 473 | filtersChanged = True 474 | elif sChoice.lower() == 'v': 475 | m.availability.look_up_avail(availabilities) 476 | elif sChoice.lower() == 'h': 477 | showHelp() 478 | elif sChoice.lower() == 'q': 479 | sys.exit("Bye now.") 480 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------