From 1eed90bb0ade23f73e663047650fee313449eb19 Mon Sep 17 00:00:00 2001 From: mahtin Date: Mon, 2 May 2016 22:20:15 -0700 Subject: [PATCH] CloudFlare API v4 complete rewrite. Added all API calls. Added examples. Added CLU tool. Added initial tests. Updated README --- CloudFlare/__init__.py | 297 ++++++++++++++++ CloudFlare/exceptions.py | 18 + {cloudflare_v4 => CloudFlare}/logger.py | 0 CloudFlare/read_configs.py | 35 ++ {cloudflare_v4 => CloudFlare}/utils.py | 5 + README.md | 346 ++++++++++++++++++- cli4/__init__.py | 0 cli4/__main__.py | 14 + cli4/cli4.py | 218 ++++++++++++ cloudflare_v4/__init__.py | 110 ------ cloudflare_v4/exceptions.py | 11 - examples/example-are-zones-ipv6-simple.py | 21 ++ examples/example-are-zones-ipv6.py | 61 ++++ examples/example-certificates.py | 60 ++++ examples/example-create-zone-and-populate.py | 72 ++++ examples/example-dnssec-settings.py | 47 +++ examples/example-ips.py | 27 ++ examples/example-settings.py | 55 +++ examples/example-user.py | 138 ++++++++ examples/example-zones.py | 60 ++++ setup.py | 21 +- tests/test1.py | 14 + 22 files changed, 1499 insertions(+), 131 deletions(-) create mode 100644 CloudFlare/__init__.py create mode 100644 CloudFlare/exceptions.py rename {cloudflare_v4 => CloudFlare}/logger.py (100%) create mode 100644 CloudFlare/read_configs.py rename {cloudflare_v4 => CloudFlare}/utils.py (65%) create mode 100644 cli4/__init__.py create mode 100755 cli4/__main__.py create mode 100644 cli4/cli4.py delete mode 100644 cloudflare_v4/__init__.py delete mode 100644 cloudflare_v4/exceptions.py create mode 100755 examples/example-are-zones-ipv6-simple.py create mode 100755 examples/example-are-zones-ipv6.py create mode 100755 examples/example-certificates.py create mode 100755 examples/example-create-zone-and-populate.py create mode 100755 examples/example-dnssec-settings.py create mode 100755 examples/example-ips.py create mode 100755 examples/example-settings.py create mode 100755 examples/example-user.py create mode 100755 examples/example-zones.py create mode 100755 tests/test1.py diff --git a/CloudFlare/__init__.py b/CloudFlare/__init__.py new file mode 100644 index 0000000..9513386 --- /dev/null +++ b/CloudFlare/__init__.py @@ -0,0 +1,297 @@ +# all the exceptions + +from logger import Logger +from utils import sanitize_secrets +from read_configs import read_configs + +from exceptions import CloudFlareError, CloudFlareAPIError, CloudFlareInternalError + +import json +import requests +import urllib + +BASE_URL = 'https://api.cloudflare.com/client/v4' + +class CloudFlare(object): + class _base: + def __init__(self, email, token, certtoken, base_url, debug): + self.EMAIL = email + self.TOKEN = token + self.CERTTOKEN = certtoken + self.BASE_URL = base_url + + if debug: + self.logger = logger.Logger(debug).getLogger() + else: + self.logger = None + + def _call_with_no_auth(self, method, api_call_part1, api_call_part2=None, identifier1=None, identifier2=None, params=None, data=None): + headers = {} + return self._call(method, headers, api_call_part1, api_call_part2, identifier1, identifier2, params, data) + + def _call_with_auth(self, method, api_call_part1, api_call_part2=None, identifier1=None, identifier2=None, params=None, data=None): + if self.EMAIL is '' or self.TOKEN is '': + raise CloudFlareAPIError(0, 'no email and/or token defined') + headers = { "X-Auth-Email": self.EMAIL, "X-Auth-Key": self.TOKEN, 'Content-Type': 'application/json' } + return self._call(method, headers, api_call_part1, api_call_part2, identifier1, identifier2, params, data) + + def _call_with_certauth(self, method, api_call_part1, api_call_part2=None, identifier1=None, identifier2=None, params=None, data=None): + if self.EMAIL is '' or self.CERTTOKEN is '': + raise CloudFlareAPIError(0, 'no email and/or cert token defined') + headers = { "X-Auth-Email": self.EMAIL, "X-Auth-User-Service-Key": self.CERTTOKEN, 'Content-Type': 'application/json' } + return self._call(method, headers, api_call_part1, api_call_part2, identifier1, identifier2, params, data) + + def _call(self, method, headers, api_call_part1, api_call_part2=None, identifier1=None, identifier2=None, params=None, data=None): + if api_call_part2 is not None or (data is not None and method == 'GET'): + if identifier2 is None: + url = self.BASE_URL + '/' + api_call_part1 + '/' + identifier1 + '/' + api_call_part2 + else: + url = self.BASE_URL + '/' + api_call_part1 + '/' + identifier1 + '/' + api_call_part2 + '/' + identifier2 + else: + if identifier1 is None: + url = self.BASE_URL + '/' + api_call_part1 + else: + url = self.BASE_URL + '/' + api_call_part1 + '/' + identifier1 + + if self.logger: + self.logger.debug("Call: %s,%s,%s,%s" % (str(api_call_part1), str(identifier1), str(api_call_part2), str(identifier2))) + self.logger.debug("Call: optional params and data: %s %s" % (str(params), str(data))) + self.logger.debug("Call: url is: %s" % (str(url))) + self.logger.debug("Call: method is: %s" % (str(method))) + self.logger.debug("Call: headers %s" % str(utils.sanitize_secrets(headers))) + + if (method is None) or (api_call_part1 is None): + raise CloudFlareInternalError('You must specify a method and endpoint') # should never happen + + method = method.upper() + + if method == 'GET': + response = requests.get(url, headers=headers, params=params, data=data) + elif method == 'POST': + response = requests.post(url, headers=headers, json=data) + elif method == 'PUT': + response = requests.put(url, headers=headers, json=data) + elif method == 'DELETE': + if data: + response = requests.delete(url, headers=headers, json=data) + else: + response = requests.delete(url, headers=headers) + elif method == 'PATCH': + if data: + response = requests.request('PATCH', url, headers=headers, params=params, json=data) + else: + response = requests.request('PATCH', url, headers=headers, params=params) + else: + raise CloudFlareAPIError(0, 'method not supported') # should never happen + + if self.logger: + self.logger.debug("Response url: %s", response.url) + + response_data = response.text + if self.logger: + self.logger.debug("Response_data: %s" % response_data) + try: + response_data = json.loads(response_data) + except ValueError: + raise CloudFlareAPIError(0, 'JSON parse failed.') + + if response_data['success'] is False: + if self.logger: + self.logger.debug("response_data error: %d %s" % (response_data['errors'][0]['code'], response_data['errors'][0]['message'])) + raise CloudFlareAPIError(response_data['errors'][0]['code'], response_data['errors'][0]['message']) + + return response_data['result'] + + class _unused: + def __init__(self, base, api_call_part1, api_call_part2=None): + #if self.logger: + # self.logger.debug("_unused %s,%s,%s" % (str(base), str(api_call_part1), str(api_call_part2))) + self.base = base + self.api_call_part1 = api_call_part1 + self.api_call_part2 = api_call_part2 + + class _client_noauth: + def __init__(self, base, api_call_part1, api_call_part2=None): + #if self.logger: + # self.logger.debug("_client_noauth %s,%s,%s" % (str(base), str(api_call_part1), str(api_call_part2))) + self.base = base + self.api_call_part1 = api_call_part1 + self.api_call_part2 = api_call_part2 + + def get(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_no_auth('GET', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + class _client_with_auth: + def __init__(self, base, api_call_part1, api_call_part2=None): + #if self.logger: + # self.logger.debug("_client_with_auth %s,%s,%s" % (str(base), str(api_call_part1), str(api_call_part2))) + self.base = base + self.api_call_part1 = api_call_part1 + self.api_call_part2 = api_call_part2 + + def get(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_auth('GET', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def patch(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_auth('PATCH', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def post(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_auth('POST', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def put(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_auth('PUT', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def delete(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_auth('DELETE', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + class _client_with_cert_auth: + def __init__(self, base, api_call_part1, api_call_part2=None): + #if self.logger: + # self.logger.debug("_client_with_cert_auth %s,%s,%s" % (str(base), str(api_call_part1), str(api_call_part2))) + self.base = base + self.api_call_part1 = api_call_part1 + self.api_call_part2 = api_call_part2 + + def get(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_certauth('GET', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def patch(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_certauth('PATCH', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def post(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_certauth('POST', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def put(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_certauth('PUT', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def delete(self, identifier1=None, identifier2=None, params=None, data=None): + return self.base._call_with_certauth('DELETE', self.api_call_part1, self.api_call_part2, identifier1, identifier2, params, data) + + def __init__(self, email=None, token=None, certtoken=None, debug=False): + base_url = BASE_URL + + # class creation values override configuration values + [ conf_email, conf_token, conf_certtoken ] = read_configs() + + if email is None: + email = conf_email + if token is None: + token = conf_token + if certtoken is None: + certtoken = conf_certtoken + + #if email is None or token is None: + # raise CloudFlareInternalError('You must at least specify an email and token string') + + self.base = self._base(email, token, certtoken, base_url, debug) + + # The API commands for /user/ + setattr(self, "user", self._client_with_auth(self.base, "user")) + user = getattr(self, "user") + setattr(user, "billing", self._unused(self.base, "user/billing")) + user_billing = getattr(user, "billing") + setattr(user_billing, "history", self._client_with_auth(self.base, "user/billing/history")) + setattr(user_billing, "profile", self._client_with_auth(self.base, "user/billing/profile")) + setattr(user_billing, "subscriptions", self._unused(self.base, "user/billing/subscriptions")) + user_billing_subscriptions = getattr(user_billing, "subscriptions") + setattr(user_billing_subscriptions, "apps", self._client_with_auth(self.base, "user/billing/subscriptions/apps")) + setattr(user_billing_subscriptions, "zones", self._client_with_auth(self.base, "user/billing/subscriptions/zones")) + setattr(user, "firewall", self._unused(self.base, "user/firewall")) + user_firewall = getattr(user, "firewall") + setattr(user_firewall, "access_rules", self._unused(self.base, "user/firewall/access_rules")) + user_firewall_access_rules = getattr(user_firewall, "access_rules") + setattr(user_firewall_access_rules, "rules", self._client_with_auth(self.base, "user/firewall/access_rules/rules")) + setattr(user, "organizations", self._client_with_auth(self.base, "user/organizations")) + setattr(user, "invites", self._client_with_auth(self.base, "user/invites")) + setattr(user, "virtual_dns", self._client_with_auth(self.base, "user/virtual_dns")) + + # The API commands for /zones/ + setattr(self, "zones", self._client_with_auth(self.base, "zones")) + zones = getattr(self, "zones") + setattr(zones, "activation_check", self._client_with_auth(self.base, "zones", "activation_check")) + setattr(zones, "available_plans", self._client_with_auth(self.base, "zones", "available_plans")) + setattr(zones, "custom_certificates", self._client_with_auth(self.base, "zones", "custom_certificates")) + zones_custom_certificates = getattr(zones, "custom_certificates") + setattr(zones_custom_certificates, "prioritize", self._client_with_auth(self.base, "zones", "custom_certificates/prioritize")) + setattr(zones, "custom_pages", self._client_with_auth(self.base, "zones", "custom_pages")) + setattr(zones, "dns_records", self._client_with_auth(self.base, "zones", "dns_records")) + setattr(zones, "keyless_certificates", self._client_with_auth(self.base, "zones", "keyless_certificates")) + setattr(zones, "pagerules", self._client_with_auth(self.base, "zones", "pagerules")) + setattr(zones, "purge_cache", self._client_with_auth(self.base, "zones", "purge_cache")) + setattr(zones, "railguns", self._client_with_auth(self.base, "zones", "railguns")) + setattr(zones, "settings", self._client_with_auth(self.base, "zones", "settings")) + zones_settings = getattr(zones, "settings") + setattr(zones_settings, "advanced_ddos", self._client_with_auth(self.base, "zones", "settings/advanced_ddos")) + setattr(zones_settings, "always_online", self._client_with_auth(self.base, "zones", "settings/always_online")) + setattr(zones_settings, "browser_cache_ttl", self._client_with_auth(self.base, "zones", "settings/browser_cache_ttl")) + setattr(zones_settings, "browser_check", self._client_with_auth(self.base, "zones", "settings/browser_check")) + setattr(zones_settings, "cache_level", self._client_with_auth(self.base, "zones", "settings/cache_level")) + setattr(zones_settings, "challenge_ttl", self._client_with_auth(self.base, "zones", "settings/challenge_ttl")) + setattr(zones_settings, "development_mode", self._client_with_auth(self.base, "zones", "settings/development_mode")) + setattr(zones_settings, "email_obfuscation", self._client_with_auth(self.base, "zones", "settings/email_obfuscation")) + setattr(zones_settings, "hotlink_protection", self._client_with_auth(self.base, "zones", "settings/hotlink_protection")) + setattr(zones_settings, "ip_geolocation", self._client_with_auth(self.base, "zones", "settings/ip_geolocation")) + setattr(zones_settings, "ipv6", self._client_with_auth(self.base, "zones", "settings/ipv6")) + setattr(zones_settings, "minify", self._client_with_auth(self.base, "zones", "settings/minify")) + setattr(zones_settings, "mirage", self._client_with_auth(self.base, "zones", "settings/mirage")) + setattr(zones_settings, "mobile_redirect", self._client_with_auth(self.base, "zones", "settings/mobile_redirect")) + setattr(zones_settings, "origin_error_page_pass_thru", self._client_with_auth(self.base, "zones", "settings/origin_error_page_pass_thru")) + setattr(zones_settings, "polish", self._client_with_auth(self.base, "zones", "settings/polish")) + setattr(zones_settings, "prefetch_preload", self._client_with_auth(self.base, "zones", "settings/prefetch_preload")) + setattr(zones_settings, "response_buffering", self._client_with_auth(self.base, "zones", "settings/response_buffering")) + setattr(zones_settings, "rocket_loader", self._client_with_auth(self.base, "zones", "settings/rocket_loader")) + setattr(zones_settings, "security_header", self._client_with_auth(self.base, "zones", "settings/security_header")) + setattr(zones_settings, "security_level", self._client_with_auth(self.base, "zones", "settings/security_level")) + setattr(zones_settings, "server_side_exclude", self._client_with_auth(self.base, "zones", "settings/server_side_exclude")) + setattr(zones_settings, "sort_query_string_for_cache", self._client_with_auth(self.base, "zones", "settings/sort_query_string_for_cache")) + setattr(zones_settings, "ssl", self._client_with_auth(self.base, "zones", "settings/ssl")) + setattr(zones_settings, "tls_1_2_only", self._client_with_auth(self.base, "zones", "settings/tls_1_2_only")) + setattr(zones_settings, "tls_client_auth", self._client_with_auth(self.base, "zones", "settings/tls_client_auth")) + setattr(zones_settings, "true_client_ip_header", self._client_with_auth(self.base, "zones", "settings/true_client_ip_header")) + setattr(zones_settings, "waf", self._client_with_auth(self.base, "zones", "settings/waf")) + setattr(zones, "analytics", self._unused(self.base, "zones", "analytics")) + zones_analytics = getattr(zones, "analytics") + setattr(zones_analytics, "colos", self._client_with_auth(self.base, "zones", "analytics/colos")) + setattr(zones_analytics, "dashboard", self._client_with_auth(self.base, "zones", "analytics/dashboard")) + setattr(zones, "firewall", self._unused(self.base, "zones", "firewall")) + zones_firewall = getattr(zones, "firewall") + setattr(zones_firewall, "access_rules", self._unused(self.base, "zones", "firewall/access_rules")) + zones_firewall_access_rules = getattr(zones_firewall, "access_rules") + setattr(zones_firewall_access_rules, "rules", self._client_with_auth(self.base, "zones", "firewall/access_rules/rules")) + + # The API commands for /railguns/ + setattr(self, "railguns", self._client_with_auth(self.base, "railguns")) + railguns = getattr(self, "railguns") + setattr(railguns, "zones", self._client_with_auth(self.base, "railguns", "zones")) + + # The API commands for /organizations/ + setattr(self, "organizations", self._client_with_auth(self.base, "organizations")) + organizations = getattr(self, "organizations") + setattr(organizations, "members", self._client_with_auth(self.base, "organizations", "members")) + setattr(organizations, "invites", self._client_with_auth(self.base, "organizations", "invites")) + setattr(organizations, "railguns", self._client_with_auth(self.base, "organizations", "railguns")) + setattr(organizations, "roles", self._client_with_auth(self.base, "organizations", "roles")) + setattr(organizations, "firewall", self._unused(self.base, "organizations", "firewall")) + organizations_firewall = getattr(organizations, "firewall") + setattr(organizations_firewall, "access_rules", self._unused(self.base, "organizations", "firewall/access_rules")) + organizations_firewall_access_rules = getattr(organizations_firewall, "access_rules") + setattr(organizations_firewall_access_rules, "rules", self._client_with_auth(self.base, "organizations", "firewall/access_rules/rules")) + setattr(organizations, "virtual_dns", self._client_with_auth(self.base, "organizations", "virtual_dns")) + + # The API commands for /certificates/ + setattr(self, "certificates", self._client_with_cert_auth(self.base, "certificates")) + + # The API commands for /ips/ + setattr(self, "ips", self._client_noauth(self.base, "ips")) + + # The DNSSEC commands + setattr(zones, "dnssec", self._client_with_auth(self.base, "zones", "dnssec")) + zones_dnssec = getattr(zones, "dnssec") + setattr(zones_dnssec, "status", self._client_with_auth(self.base, "zones", "dnssec/status")) + + # EXTRAS + # /zones/:zone_id/ssl/certificate_packs + setattr(zones, "ssl", self._client_with_auth(self.base, "zones", "ssl")) + zones_ssl = getattr(zones, "ssl") + setattr(zones_ssl, "certificate_packs", self._client_with_auth(self.base, "zones", "ssl/certificate_packs")) + diff --git a/CloudFlare/exceptions.py b/CloudFlare/exceptions.py new file mode 100644 index 0000000..5bd3928 --- /dev/null +++ b/CloudFlare/exceptions.py @@ -0,0 +1,18 @@ + +class CloudFlareError(Exception): + def __init__(self, code, message): + self.code = code + self.message = message + + def __int__(self): + return self.code + + def __str__(self): + return self.message + +class CloudFlareAPIError(CloudFlareError): + pass + +class CloudFlareInternalError(CloudFlareError): + pass + diff --git a/cloudflare_v4/logger.py b/CloudFlare/logger.py similarity index 100% rename from cloudflare_v4/logger.py rename to CloudFlare/logger.py diff --git a/CloudFlare/read_configs.py b/CloudFlare/read_configs.py new file mode 100644 index 0000000..e5c2496 --- /dev/null +++ b/CloudFlare/read_configs.py @@ -0,0 +1,35 @@ +import os +import re +import ConfigParser + +def read_configs(): + + # envioronment variables override config files + email = os.getenv('CF_API_EMAIL') + token = os.getenv('CF_API_KEY') + certtoken = os.getenv('CF_API_CERTKEY') + + # grab values from config files + config = ConfigParser.RawConfigParser() + config.read(['.cloudflare.cfg', os.path.expanduser('~/.cloudflare.cfg'), os.path.expanduser('~/.cloudflare/cloudflare.cfg')]) + + if email is None: + try: + email = re.sub(r"\s+", '', config.get('CloudFlare', 'email')) + except: + email = None + + if token is None: + try: + token = re.sub(r"\s+", '', config.get('CloudFlare', 'token')) + except: + token = None + + if certtoken is None: + try: + certtoken = re.sub(r"\s+", '', config.get('CloudFlare', 'certtoken')) + except: + certtoken = None + + return [ email, token, certtoken ] + diff --git a/cloudflare_v4/utils.py b/CloudFlare/utils.py similarity index 65% rename from cloudflare_v4/utils.py rename to CloudFlare/utils.py index 64ec318..e3819b3 100644 --- a/cloudflare_v4/utils.py +++ b/CloudFlare/utils.py @@ -1,10 +1,15 @@ def sanitize_secrets(secrets): redacted_phrase = 'REDACTED' + if secrets is None: + return None + secrets_copy = secrets.copy() if 'password' in secrets_copy: secrets_copy['password'] = redacted_phrase elif 'X-Auth-Key' in secrets_copy: secrets_copy['X-Auth-Key'] = redacted_phrase + elif 'X-Auth-User-Service-Key' in secrets_copy: + secrets_copy['X-Auth-User-Service-Key'] = redacted_phrase return secrets_copy diff --git a/README.md b/README.md index d2dffbd..2083a8f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,344 @@ -python-cloudflare-v4 -==================== +# cloudflare -very rough and still under development + +## Credit + +This is based on work by _gnowxilef_ found [here](https://github.com/cloudflare-api/python-cloudflare-v4). It has been seriously expanded upon. + +## CloudFlare API version 4 + +The CloudFlare API can be found [here](https://api.cloudflare.com/). Each API call is provided via a similarly named function within the _CloudFlare_ class. A full list is provided below. + +## Installation + +``` + ./setup.py install +``` + +## Getting Started + +A very simple listing of zones within your account; including the IPv6 status of the zone. + +``` +from cloudflare_v4 import CloudFlare + +def main(): + cf = CloudFlare() + zones = cf.zones.get(param={'per_page':50}) + for zone in zones: + zone_name = zone['name'] + zone_id = zone['id'] + settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) + ipv6_status = settings_ipv6['value'] + settings_ssl = cf.zones.settings.ssl.get(zone_id) + ssl_status = settings_ssl['value'] + print zone_id, ssl_status, ipv6_status, zone_name + +if __name__ == '__main__': + main() +``` + +A more complex example follows. + +``` + zone_name = 'example.com' + + cf = CloudFlare() + + # query for the zone name and expect only one value back + try: + zones = cf.zones.get(params = {'name':zone_name,'per_page':1}) + except ConnectionError, e: + # unable to connect to CloudFlare - possibly you're offline + print '/zones - api call failed' + exit(1) + except Exception, e: + print '/zones - api call failed' + exit(1) + + # extract the zone_id which is needed to process that zone + zone = zones[0] + zone_id = zone['id'] + + # request the DNS records from that zone + try: + dns_records = cf.zones.dns_records.get(zone_id) + except Exception, e: + print '/zones/dns_records - api call failed' + exit(1) + + # print the results - first the zone name + print zone_id, zone_name + + # then all the DNS records for that zone + for dns_record in dns_records: + r_name = dns_record['name'] + r_type = dns_record['type'] + r_value = dns_record['content'] + r_id = dns_record['id'] + print '\t', r_id, r_name, r_type, r_value + + +``` + +## Providing CloudFlare Username and API Key + +When you create a _CloudFlare_ class you can pass up to three paramaters. + + * Account email + * Account API key + * Optional Debug flag (True/False) + +If the account email and API key are not passed when you create the class, then they are retreived from either the users exported shell environment variables or the .cloudflare.cfg or ~/.cloudflare.cfg or ~/.cloudflare/cloudflare.cfg files, in that order. + +### Using shell environment variables +``` +$ export CF_API_EMAIL='user@example.com' +$ export CF_API_KEY='00000000000000000000000000000000' +$ +``` + +### Using configuration file to store email and keys + +``` +$ cat ~/.cloudflare/cloudflare.cfg +[CloudFlare] +email = user@example.com +token = 00000000000000000000000000000000 +$ +``` + +## Included example code + +The _example_ folder contains many examples in both simple and verbose formats. + +## CLI + +All API calls can be called from the command line. The command will convert domain names on-the-fly into zone_identifier's. + +``` +$ cli4 [--help] [--verbose] [--get|--put|--patch|--delete] command + +``` + +Sample commands include + + * ```cli4 /user/billing/profile``` + * ```cli4 /user/invites``` + + * ```cli4 /zones/:example.com``` + * ```cli4 /zones/:example.com/dnssec``` + * ```cli4 /zones/:example.com/settings/ipv6``` + * ```cli4 --put /zones/:example.com/activation_check``` + * ```cli4 /zones/:example.com/keyless_certificates``` + + * ```cli4 /zones/:asntryst.com/analytics/dashboard``` + +The output from the CLI command is in json format (and human readable). + +## Implemented API calls + +``` + GET /user + PATCH /user + + GET /user/billing/history + GET /user/billing/profile + GET /user/billing/subscriptions/apps + GET /user/billing/subscriptions/apps/:identifier + GET /user/billing/subscriptions/zones + GET /user/billing/subscriptions/zones/:identifier + + GET /user/firewall/access_rules/rules + POST /user/firewall/access_rules/rules + PATCH /user/firewall/access_rules/rules/:identifier + DELETE /user/firewall/access_rules/rules/:identifier + + GET /user/organizations + GET /user/organizations/:organization_identifier + DELETE /user/organizations/:organization_identifier + + GET /user/invites + GET /user/invites/:identifier + PATCH /user/invites/:identifier + + GET /zones + POST /zones + GET /zones/:zone_identifier + PATCH /zones/:zone_identifier + DELETE /zones/:zone_identifier + + PUT /zones/:zone_identifier/activation_check + + GET /zones/:zone_identifier/analytics/colos + GET /zones/:zone_identifier/analytics/dashboard + + GET /zones/:zone_identifier/available_plans + GET /zones/:zone_identifier/available_plans/:identifier + + GET /zones/:zone_identifier/dns_records + POST /zones/:zone_identifier/dns_records + GET /zones/:zone_identifier/dns_records/:identifier + PUT /zones/:zone_identifier/dns_records/:identifier + DELETE /zones/:zone_identifier/dns_records/:identifier + + DELETE /zones/:zone_identifier/purge_cache + + GET /zones/:zone_identifier/railguns + GET /zones/:zone_identifier/railguns/:identifier + PATCH /zones/:zone_identifier/railguns/:identifier + GET /zones/:zone_identifier/railguns/:identifier/diagnose + + GET /zones/:zone_identifier/settings + PATCH /zones/:zone_identifier/settings + GET /zones/:zone_identifier/settings/advanced_ddos + GET /zones/:zone_identifier/settings/always_online + PATCH /zones/:zone_identifier/settings/always_online + GET /zones/:zone_identifier/settings/browser_cache_ttl + PATCH /zones/:zone_identifier/settings/browser_cache_ttl + GET /zones/:zone_identifier/settings/browser_check + PATCH /zones/:zone_identifier/settings/browser_check + GET /zones/:zone_identifier/settings/cache_level + PATCH /zones/:zone_identifier/settings/cache_level + GET /zones/:zone_identifier/settings/challenge_ttl + PATCH /zones/:zone_identifier/settings/challenge_ttl + GET /zones/:zone_identifier/settings/development_mode + PATCH /zones/:zone_identifier/settings/development_mode + GET /zones/:zone_identifier/settings/email_obfuscation + PATCH /zones/:zone_identifier/settings/email_obfuscation + GET /zones/:zone_identifier/settings/hotlink_protection + PATCH /zones/:zone_identifier/settings/hotlink_protection + GET /zones/:zone_identifier/settings/ip_geolocation + PATCH /zones/:zone_identifier/settings/ip_geolocation + GET /zones/:zone_identifier/settings/ipv6 + PATCH /zones/:zone_identifier/settings/ipv6 + GET /zones/:zone_identifier/settings/minify + PATCH /zones/:zone_identifier/settings/minify + GET /zones/:zone_identifier/settings/mirage + PATCH /zones/:zone_identifier/settings/mirage + GET /zones/:zone_identifier/settings/mobile_redirect + PATCH /zones/:zone_identifier/settings/mobile_redirect + GET /zones/:zone_identifier/settings/origin_error_page_pass_thru + PATCH /zones/:zone_identifier/settings/origin_error_page_pass_thru + GET /zones/:zone_identifier/settings/polish + PATCH /zones/:zone_identifier/settings/polish + GET /zones/:zone_identifier/settings/prefetch_preload + PATCH /zones/:zone_identifier/settings/prefetch_preload + GET /zones/:zone_identifier/settings/response_buffering + PATCH /zones/:zone_identifier/settings/response_buffering + GET /zones/:zone_identifier/settings/rocket_loader + PATCH /zones/:zone_identifier/settings/rocket_loader + GET /zones/:zone_identifier/settings/security_header + PATCH /zones/:zone_identifier/settings/security_header + GET /zones/:zone_identifier/settings/security_level + PATCH /zones/:zone_identifier/settings/security_level + GET /zones/:zone_identifier/settings/server_side_exclude + PATCH /zones/:zone_identifier/settings/server_side_exclude + GET /zones/:zone_identifier/settings/sort_query_string_for_cache + PATCH /zones/:zone_identifier/settings/sort_query_string_for_cache + GET /zones/:zone_identifier/settings/ssl + PATCH /zones/:zone_identifier/settings/ssl + GET /zones/:zone_identifier/settings/tls_1_2_only + PATCH /zones/:zone_identifier/settings/tls_1_2_only + GET /zones/:zone_identifier/settings/tls_client_auth + PATCH /zones/:zone_identifier/settings/tls_client_auth + GET /zones/:zone_identifier/settings/true_client_ip_header + PATCH /zones/:zone_identifier/settings/true_client_ip_header + GET /zones/:zone_identifier/settings/waf + PATCH /zones/:zone_identifier/settings/waf + + GET /railguns + POST /railguns + GET /railguns/:identifier + PATCH /railguns/:identifier + DELETE /railguns/:identifier + GET /railguns/:identifier/zones + + GET /zones/:zone_identifier/firewall/access_rules/rules + POST /zones/:zone_identifier/firewall/access_rules/rules + PATCH /zones/:zone_identifier/firewall/access_rules/rules/:identifier + DELETE /zones/:zone_identifier/firewall/access_rules/rules/:identifier + + GET /zones/:zone_identifier/firewall/waf/packages/:package_identifier/rules + GET /zones/:zone_identifier/firewall/waf/packages/:package_identifier/rules/:identifier + PATCH /zones/:zone_identifier/firewall/waf/packages/:package_identifier/rules/:identifier + + GET /zones/:zone_identifier/custom_certificates + POST /zones/:zone_identifier/custom_certificates + GET /zones/:zone_identifier/custom_certificates/:identifier + PATCH /zones/:zone_identifier/custom_certificates/:identifier + DELETE /zones/:zone_identifier/custom_certificates/:identifier + PUT /zones/:zone_identifier/custom_certificates/prioritize + + GET /zones/:zone_identifier/custom_pages + GET /zones/:zone_identifier/custom_pages/:identifier + PUT /zones/:zone_identifier/custom_pages/:identifier + + GET /zones/:zone_identifier/firewall/waf/packages + GET /zones/:zone_identifier/firewall/waf/packages/:identifier + PATCH /zones/:zone_identifier/firewall/waf/packages/:identifier + GET /zones/:zone_identifier/firewall/waf/packages/:package_identifier/groups + GET /zones/:zone_identifier/firewall/waf/packages/:package_identifier/groups/:identifier + PATCH /zones/:zone_identifier/firewall/waf/packages/:package_identifier/groups/:identifier + + GET /zones/:zone_identifier/keyless_certificates + POST /zones/:zone_identifier/keyless_certificates + GET /zones/:zone_identifier/keyless_certificates/:identifier + PATCH /zones/:zone_identifier/keyless_certificates/:identifier + DELETE /zones/:zone_identifier/keyless_certificates/:identifier + + GET /zones/:zone_identifier/pagerules + POST /zones/:zone_identifier/pagerules + GET /zones/:zone_identifier/pagerules/:identifier + PUT /zones/:zone_identifier/pagerules/:identifier + PATCH /zones/:zone_identifier/pagerules/:identifier + DELETE /zones/:zone_identifier/pagerules/:identifier + + GET /organizations/:organization_identifier + PATCH /organizations/:organization_identifier + + GET /organizations/:organization_identifier/members + GET /organizations/:organization_identifier/members/:identifier + PATCH /organizations/:organization_identifier/members/:identifier + DELETE /organizations/:organization_identifier/members/:identifier + + GET /organizations/:organization_identifier/invites + POST /organizations/:organization_identifier/invites + GET /organizations/:organization_identifier/invites/:identifier + PATCH /organizations/:organization_identifier/invites/:identifier + DELETE /organizations/:organization_identifier/invites/:identifier + + GET /organizations/:organization_identifier/roles + GET /organizations/:organization_identifier/roles/:identifier + + GET /organizations/:organization_identifier/firewall/access_rules/rules + POST /organizations/:organization_identifier/firewall/access_rules/rules + PATCH /organizations/:organization_identifier/firewall/access_rules/rules/:identifier + DELETE /organizations/:organization_identifier/firewall/access_rules/rules/:identifier + + GET /organizations/:organization_identifier/railguns + POST /organizations/:organization_identifier/railguns + GET /organizations/:organization_identifier/railguns/:identifier + GET /organizations/:organization_identifier/railguns/:identifier/zones + PATCH /organizations/:organization_identifier/railguns/:identifier + DELETE /organizations/:organization_identifier/railguns/:identifier + + GET /certificates + POST /certificates + GET /certificates/:identifier + DELETE /certificates/:identifier + + GET /user/virtual_dns + POST /user/virtual_dns + GET /user/virtual_dns/:identifier + PATCH /user/virtual_dns/:identifier + DELETE /user/virtual_dns/:identifier + + GET /organizations/:organization_identifier/virtual_dns + POST /organizations/:organization_identifier/virtual_dns + GET /organizations/:organization_identifier/virtual_dns/:identifier + PATCH /organizations/:organization_identifier/virtual_dns/:identifier + DELETE /organizations/:organization_identifier/virtual_dns/:identifier + + GET /ips +``` diff --git a/cli4/__init__.py b/cli4/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli4/__main__.py b/cli4/__main__.py new file mode 100755 index 0000000..24ca60c --- /dev/null +++ b/cli4/__main__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import sys + +from cli4 import cli4 + +def main(args=None): + if args is None: + args = sys.argv[1:] + cli4(args) + +if __name__ == '__main__': + main() + diff --git a/cli4/cli4.py b/cli4/cli4.py new file mode 100644 index 0000000..bc44b51 --- /dev/null +++ b/cli4/cli4.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +import re +import json +import getopt + +def convert_zones_to_identifier(cf, zone_name): + params = {'name':zone_name,'per_page':1} + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: %s - %d %s' % (zone_name, e, e)) + except Exception as e: + exit('cli4: %s - %s' % (zone_name, e)) + + for zone in zones: + name = zone['name'] + if zone_name == name: + zone_id = zone['id'] + return zone_id + + exit('cli4: %s - zone not found' % (zone_name)) + +def convert_certificates_to_identifier(cf, certificate_name): + try: + certificates = cf.certificates.get() + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: %s - %d %s' % (certificate_name, e, e)) + except Exception as e: + exit('cli4: %s - %s' % (certificate_name, e)) + + for certificate in certificates: + hostnames = certificate['hostnames'] + if certificate_name in hostnames: + certificate_id = certificate['id'] + return certificate_id + + exit('cli4: %s - no zone certificates found' % (certificate_name)) + +def convert_organizations_to_identifier(cf, organization_name): + try: + organizations = cf.user.organizations.get() + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: %s - %d %s' % (organization_name, e, e)) + except Exception as e: + exit('cli4: %s - %s' % (organization_name, e)) + + for organization in organizations: + name = organization['name'] + if organization_name == name : + organization_id = organization['id'] + return organization_id + + exit('cli4: %s - no organizations found' % (organization_name)) + +def convert_invites_to_identifier(cf, invite_name): + try: + invites = cf.user.invites.get() + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: %s - %d %s' % (invite_name, e, e)) + except Exception as e: + exit('cli4: %s - %s' % (invite_name, e)) + + for invite in invites: + name = invite['organization_name'] + if invite_name == name : + invite_id = invite['id'] + return invite_id + + exit('cli4: %s - no invites found' % (invite_name)) + +def convert_virtual_dns_to_identifier(cf, virtual_dns_name): + try: + virtual_dnss = cf.user.virtual_dns.get() + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: %s - %d %s\n' % (virtual_dns_name, e, e)) + except Exception as e: + exit('cli4: %s - %s\n' % (virtual_dns_name, e)) + + for virtual_dns in virtual_dnss: + name = virtual_dns['name'] + if virtual_dns_name == name : + virtual_dns_id = virtual_dns['id'] + return virtual_dns_id + + exit('cli4: %s - no virtual_dns found' % (virtual_dns_name)) + +def cli4(args): + verbose = False + quiet = False + method = 'GET' + + usage = 'usage: cli4 [-h|--help] [-v|--verbose] [-q|--quiet] [--get|--patch|--post|-put|--delete] /command...' + + try: + opts, args = getopt.getopt(args, 'hvq', ['help', 'verbose', 'quiet', 'get', 'patch', 'post', 'put', 'delete']) + except getopt.GetoptError: + exit(usage) + for opt, arg in opts: + if opt in ('-h', '--help'): + exit(usage) + elif opt in ('-v', '--verbose'): + verbose = True + elif opt in ('-q', '--quiet'): + quiet = True + elif opt in ('--get'): + method = 'GET' + elif opt in ('-P', '--patch'): + method = 'PATCH' + elif opt in ('-O', '--post'): + method = 'POST' + elif opt in ('-U', '--put'): + method = 'PUT' + elif opt in ('-D', '--delete'): + method = 'DELETE' + + # next grab the params. These are in the form of tag=value + params = {} + while len(args) > 0 and '=' in args[0]: + tag, value = args.pop(0).split('=') + if value == 'true': + value = True + elif value == 'false': + value = False + # elif value.isdigit(): + # value = int(value) + elif value[0] is '[' and value[-1] is ']': + value = value[1:-1].split(',') + params[tag] = value + + # what's left is the command itself + if len(args) != 1: + exit(usage) + + command = args[0] + # remove leading and trailing /'s + if command[0] == '/': + command = command[1:] + if command[-1] == '/': + command = command[:-1] + + # bread down command into it's seperate pieces; these are then checked against the CloudFlare class to confirm there is a method that matches + parts = command.split('/') + + cmd = [] + identifier1 = None + identifier2 = None + + hex_only = re.compile('^[0-9a-fA-F]+$') + + cf = CloudFlare.CloudFlare(debug=verbose) + + m = cf + for element in parts: + if element[0] == ':': + element = element[1:] + if identifier1 is None: + if len(element) == 32 and hex_only.match(element): + # raw identifier - lets just use it as-is + identifier1 = element + elif cmd[0] == 'certificates': + # identifier1 = convert_certificates_to_identifier(cf, element) + identifier1 = convert_zones_to_identifier(cf, element) + elif cmd[0] == 'zones': + identifier1 = convert_zones_to_identifier(cf, element) + elif (cmd[0] == 'user' and cmd[1] == 'organizations') or cmd[0] == 'organizations': + identifier1 = convert_organizations_to_identifier(cf, element) + elif (cmd[0] == 'user' and cmd[1] == 'invites'): + identifier1 = convert_invites_to_identifier(cf, element) + elif (cmd[0] == 'user' and cmd[1] == 'virtual_dns'): + identifier1 = convert_virtual_dns_to_identifier(cf, element) + else: + print cmd[0], element, ':NOT CODED YET' + exit(0) + cmd.append(':' + identifier1) + else: + if len(element) == 32 and hex_only.match(element): + # raw identifier - lets just use it as-is + identifier2 = element + else: + identifier2 = convert_zones_to_identifier(cf, element) + cmd.append(':' + identifier2) + else: + try: + m = getattr(m, element) + cmd.append(element) + except: + if len(cmd) == 0: + exit('cli4: /%s - not found' % (element)) + else: + exit('cli4: /%s/%s - not found' % ('/'.join(cmd), element)) + + try: + if method is 'GET': + r = m.get(identifier1 = identifier1, identifier2 = identifier2, params = params) + elif method is 'PATCH': + r = m.patch(identifier1 = identifier1, identifier2 = identifier2, data = params) + elif method is 'POST': + r = m.post(identifier1 = identifier1, identifier2 = identifier2, data = params) + elif method is 'PUT': + r = m.put(identifier1 = identifier1, identifier2 = identifier2, params = params) + elif method is 'DELETE': + r = m.delete(identifier1 = identifier1, identifier2 = identifier2, data = params) + else: + pass + except CloudFlare.CloudFlareAPIError as e: + exit('cli4: /%s - %d %s' % (command, e, e)) + except Exception as e: + exit('cli4: /%s - %s - api error' % (command, e)) + + if not quiet: + print json.dumps(r, indent=4, sort_keys=True) + diff --git a/cloudflare_v4/__init__.py b/cloudflare_v4/__init__.py deleted file mode 100644 index 8dbc5ce..0000000 --- a/cloudflare_v4/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -# all the exceptions -from .exceptions import CloudFlareError, CloudFlareAPIError, CloudFlareInternalError -from . import logger - -import json -import requests -import urllib -from . import utils - -BASE_URL = 'https://api.cloudflare.com/client/v4' - -class CloudFlare(object): - class BaseClient: - def __init__(self, email, token, debug): - self.EMAIL = email - self.TOKEN = token - self.logger = logger.Logger(debug).getLogger() - - def call(self, method, main_endpoint, endpoint=None, params=None, data=None): - headers = { "X-Auth-Email": self.EMAIL, "X-Auth-Key": self.TOKEN } - headers['Content-Type'] = 'application/json' - if endpoint is not None or (data is not None and method == 'GET'): - url = BASE_URL + '/' + main_endpoint + '/' + params + '/' + endpoint - else: - url = BASE_URL + '/' + main_endpoint - method = method.upper() - self.logger.debug("EMAIL is: %s" % str(self.EMAIL)) - self.logger.debug("TOKEN is: %s" % str(self.TOKEN)) - self.logger.debug("method type is: %s" % method) - self.logger.debug("main endpoint is: %s" % main_endpoint) - self.logger.debug("optional endpoint is: %s" % endpoint) - self.logger.debug("url endpoint is: %s" % url) - self.logger.debug("optional params is: %s" % str(params)) - self.logger.debug("optional data is: %s" % str(data)) - if (method is None) or (main_endpoint is None): - raise CloudFlareInternalError('You must specify a method and endpoint') # should never happen - else: - self.logger.debug("headers being sent: %s" % - str(utils.sanitize_secrets(headers))) - if method == 'GET': - try: - if params.keys(): - response = requests.get(url, headers=headers, - params=params) - except AttributeError as ae: - if data: - response = requests.get(url, headers=headers, params=data) - else: - response = requests.get(url, headers=headers) - elif method == 'POST': - response = requests.post(url, headers=headers, json=data) - elif method == 'PUT': - response = requests.put(url, headers=headers, json=data) - elif method == 'DELETE': - if data: - response = requests.delete(url, headers=headers, json=data) - else: - response = requests.delete(url, headers=headers) - elif method == 'PATCH': - response = requests.request('PATCH', url, headers=headers, json=data) - else: - raise CloudFlareAPIError('method not supported') # should never happen - self.logger.debug("request url: %s", response.url) - - data = response.text - self.logger.debug("data received: %s" % data) - try: - data = json.loads(data) - if data['success'] is False: - raise CloudFlareAPIError(data['errors'][0]['message']) - else: - return data['result'] - except ValueError: - raise CloudFlareAPIError('JSON parse failed.') - - class DynamicClient: - def __init__(self, base_client, main_endpoint, endpoint=None): - base_client.logger.debug("base client is: %s" % str(base_client)) - base_client.logger.debug("main endpoint is: %s" % str(main_endpoint)) - base_client.logger.debug("endpoint is: %s" % str(endpoint)) - self.base_client = base_client - self.main_endpoint = main_endpoint - self.endpoint = endpoint - - def get(self, params=None, data=None): - return self.base_client.call('GET', self.main_endpoint, - self.endpoint, params, data) - - def post(self, params=None, data=None): - return self.base_client.call('POST', self.main_endpoint, - self.endpoint, params, data) - - def put(self, params=None, data=None): - return self.base_client.call('PUT', self.main_endpoint, - self.endpoint, params, data) - - def delete(self, params=None, data=None): - return self.base_client.call('DELETE', self.main_endpoint, - self.endpoint, params, data) - - - def __init__(self, email, token, debug): - self.base_client = self.BaseClient(email, token, debug) - setattr(self, "zones", self.DynamicClient(self.base_client, "zones")) - setattr(self, "user", self.DynamicClient(self.base_client, "user")) - zones = getattr(self, "zones") - setattr(zones, "dns_records", self.DynamicClient(self.base_client, - "zones", "dns_records")) - setattr(zones, "purge_cache", self.DynamicClient(self.base_client, - "zones", "purge_cache")) diff --git a/cloudflare_v4/exceptions.py b/cloudflare_v4/exceptions.py deleted file mode 100644 index 2e89484..0000000 --- a/cloudflare_v4/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -class CloudFlareError(Exception): - def __init__(self, value): - self.value = value - def __str__(self): - return self.value - -class CloudFlareAPIError(CloudFlareError): - pass - -class CloudFlareInternalError(CloudFlareError): - pass diff --git a/examples/example-are-zones-ipv6-simple.py b/examples/example-are-zones-ipv6-simple.py new file mode 100755 index 0000000..d9a96f4 --- /dev/null +++ b/examples/example-are-zones-ipv6-simple.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + cf = CloudFlare.CloudFlare() + zones = cf.zones.get(params={'per_page':50}) + for zone in zones: + zone_name = zone['name'] + zone_id = zone['id'] + settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) + ipv6_on = settings_ipv6['value'] + print zone_id, ipv6_on, zone_name + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-are-zones-ipv6.py b/examples/example-are-zones-ipv6.py new file mode 100755 index 0000000..07bf6ac --- /dev/null +++ b/examples/example-are-zones-ipv6.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + + # Check for update flag + update_ipv6 = False + try: + if sys.argv[1] == '--update': + update_ipv6 = True + sys.argv.pop(1) + except: + pass + + # Grab the first argument, if there is one + try: + zone_name = sys.argv[1] + params = {'name':zone_name,'per_page':1} + except: + params = {'per_page':50} + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.get %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones - %s - api call failed' % (e)) + + for zone in sorted(zones, key=lambda v: v['name']): + zone_name = zone['name'] + zone_id = zone['id'] + try: + ipv6 = cf.zones.settings.ipv6.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.settings.ipv6.get %d %s - api call failed' % (e, e)) + + ipv6_value = ipv6['value'] + if update_ipv6 and ipv6_value == 'off': + print zone_id, ipv6_value, zone_name, '(now updating... off -> on)' + try: + ipv6 = cf.zones.settings.ipv6.patch(zone_id, data={'value':'on'}) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.settings.ipv6.patch %d %s - api call failed' % (e, e)) + ipv6_value = ipv6['value'] + if ipv6_value == 'on': + print '\t', '... updated!' + else: + print zone_id, ipv6_value, zone_name + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-certificates.py b/examples/example-certificates.py new file mode 100755 index 0000000..59db745 --- /dev/null +++ b/examples/example-certificates.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +import json + +def main(): + # Grab the first argument, if there is one + try: + zone_name = sys.argv[1] + params = {'name':zone_name,'per_page':1} + except: + params = {'per_page':50} + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones - %s - api call failed' % (e)) + + # there should only be one zone + for zone in sorted(zones, key=lambda v: v['name']): + zone_name = zone['name'] + zone_id = zone['id'] + try: + certificates = cf.zones.ssl.certificate_packs.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.ssl.certificate_packs %d %s - api call failed' % (e, e)) + + for certificate in certificates: + certificate_type = certificate['type'] + primary_certificate = certificate['primary_certificate'] + certificate_hosts = certificate['hosts'] + certificate_sig = certificate['certificates'][0]['signature'] + certificate_sig_count = len(certificate['certificates']) + if certificate_sig_count > 1: + c = certificate['certificates'][0] + print '%-40s %-10s %-32s %-15s [ %s ]' %(zone_name, certificate_type, primary_certificate, c['signature'], ','.join(certificate_hosts)) + nn = 0 + for c in certificate['certificates']: + nn += 1 + if nn == 1: + next + print '%-40s %-10s %-32s %2d:%-15s [ %s ]' %('', '', '', nn, c['signature'], '') + else: + for c in certificate['certificates']: + print '%-40s %-10s %-32s %-15s [ %s ]' %(zone_name, certificate_type, primary_certificate, c['signature'], ','.join(certificate_hosts)) + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-create-zone-and-populate.py b/examples/example-create-zone-and-populate.py new file mode 100755 index 0000000..4da56a1 --- /dev/null +++ b/examples/example-create-zone-and-populate.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + try: + zone_name = sys.argv[1] + except: + exit('usage: provide a zone name as an argument on the command line') + + cf = CloudFlare.CloudFlare() + + # Create zone - which will only work if ... 1) The zone is not on CloudFlare. 2) The zone passes a whois test + print 'Create zone %s ...' % (zone_name) + try: + zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name}) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.post %s - %d %s' % (zone_name, e, e)) + except Exception as e: + exit('/zones.post %s - %s' % (zone_name, e)) + + zone_id = zone_info['id'] + if 'email' in zone_info['owner']: + zone_owner = zone_info['owner']['email'] + else: + zone_owner = '"' + zone_info['owner']['name'] + '"' + zone_plan = zone_info['plan']['name'] + zone_status = zone_info['status'] + print '\t%s name=%s owner=%s plan=%s status=%s\n' % (zone_id, zone_name, zone_owner, zone_plan, zone_status) + + # DNS records to create + dns_records = [ + {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'}, + {'name':'foo', 'type':'A', 'content':'192.168.0.1'}, + {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120}, + {'name':'bar', 'type':'CNAME', 'content':'foo.mahtin.net'}, + {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name would smell as sweet."} + ] + + print 'Create DNS records ...' + for dns_record in dns_records: + # Create DNS record + try: + r = cf.zones.dns_records.post(zone_id, data=dns_record) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.dns_records.post %s %s - %d %s' % (zone_name, record['name'], e, e)) + # Print respose info - they should be the same + dns_record = r + print '\t%s %30s %6d %-5s %s' % (dns_record['id'], dns_record['name'], dns_record['ttl'], dns_record['type'], dns_record['content']) + + print '' + + # Now read back all the DNS records + print 'Read back DNS records ...' + try: + dns_records = cf.zones.dns_records.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.dns_records.get %s - %d %s' % (zone_name, e, e)) + + for dns_record in sorted(dns_records, key=lambda v: v['name']): + print '\t%s %30s %6d %-5s %s' % (dns_record['id'], dns_record['name'], dns_record['ttl'], dns_record['type'], dns_record['content']) + + print '' + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-dnssec-settings.py b/examples/example-dnssec-settings.py new file mode 100755 index 0000000..f0f27b9 --- /dev/null +++ b/examples/example-dnssec-settings.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + # Grab the first argument, if there is one + try: + zone_name = sys.argv[1] + params = {'name':zone_name,'per_page':1} + except: + params = {'per_page':1} + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.get %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones.get - %s - api call failed' % (e)) + + # there should only be one zone + for zone in sorted(zones, key=lambda v: v['name']): + zone_name = zone['name'] + zone_id = zone['id'] + # grab the DNSSEC settings + try: + settings = cf.zones.dnssec.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.dnssec.get %d %s - api call failed' % (e, e)) + + print zone_id, zone_name + # display every setting value + for setting in sorted(settings): + print '\t%-30s %10s = %s' % (setting, '(editable)' if setting == 'status' else '' , settings[setting]) + + print '' + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-ips.py b/examples/example-ips.py new file mode 100755 index 0000000..b1f29ea --- /dev/null +++ b/examples/example-ips.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + cf = CloudFlare.CloudFlare() + try: + ips = cf.ips.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/ips - %d %s' % (e, e)) + except Exception as e: + exit('/ips - %s - api call connection failed' % (e)) + + print 'ipv4_cidrs count = ', len(ips['ipv4_cidrs']) + for cidr in sorted(set(ips['ipv4_cidrs'])): + print '\t', cidr + print 'ipv6_cidrs count = ', len(ips['ipv6_cidrs']) + for cidr in sorted(set(ips['ipv6_cidrs'])): + print '\t', cidr + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-settings.py b/examples/example-settings.py new file mode 100755 index 0000000..24ded7d --- /dev/null +++ b/examples/example-settings.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def main(): + # Grab the first argument, if there is one + try: + zone_name = sys.argv[1] + params = {'name':zone_name,'per_page':1} + except: + params = {'per_page':1} + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.get %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones.get - %s - api call failed' % (e)) + + # there should only be one zone + for zone in sorted(zones, key=lambda v: v['name']): + zone_name = zone['name'] + zone_id = zone['id'] + try: + settings = cf.zones.settings.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones.settings.get %d %s - api call failed' % (e, e)) + + print zone_id, zone_name + for setting in sorted(settings, key=lambda v: v['id']): + r_name = setting['id'] + r_value = setting['value'] + r_editable = setting['editable'] + try: + k = sorted(r_value.keys()) + print '\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '' , '{') + for k in sorted(r_value.keys()): + print '\t%-30s %10s %s = %s' % ('', '', r_name+'/'+k, r_value[k]) + print '\t%-30s %10s = %s' % ('', '', '}') + except: + print '\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '' , r_value) + + print '' + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-user.py b/examples/example-user.py new file mode 100755 index 0000000..e82654c --- /dev/null +++ b/examples/example-user.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +import json + +def main(): + + cf = CloudFlare.CloudFlare() + + print 'USER:' + # grab the user info + try: + user = cf.user.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/user.get %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/user.get - %s - api call failed' % (e)) + for k in sorted(user.keys()): + if isinstance(user[k], list): + if isinstance(user[k][0], dict): + print '\t%-40s =' % (k) + for l in user[k]: + for j in sorted(l.keys()): + if isinstance(l[j], list): + print '\t%-40s %s = [ %s ]' % ('', j, ', '.join(l[j])) + else: + print '\t%-40s %s = %s' % ('', j, l[j]) + else: + print '\t%-40s = [ %s ]' % (k, ', '.join(user[k])) + elif isinstance(user[k], dict): + print '\t%-40s =' % (k) + for j in sorted(user[k].keys()): + print '\t%-40s %s = %s' % (j, user[k][j]) + else: + print '\t%-40s = %s' % (k, user[k]) + print '' + + print 'ORGANIZATIONS:' + # grab the user organizations info + try: + organizations = cf.user.organizations.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/user.organizations.get %d %s - api call failed' % (e, e)) + if len(organizations) == 0: + print '\tNo organization' + for organization in organizations: + organization_name = organization['name'] + organization_id = organization['id'] + organization_status = organization['status'] + print '\t%-40s %-10s %s' % (organization_id, organization_status, organization_name) + print '' + + print 'INVITES:' + # grab the user invites info + try: + invites = cf.user.invites.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/user.invites.get %d %s - api call failed' % (e, e)) + if len(invites) == 0: + print '\tNo user invites' + for invite in invites: + invited_member_id = invite['invited_member_id'] + invited_member_email = invite['invited_member_email'] + organization_id = invite['organization_id'] + organization_name = invite['organization_name'] + invited_by = invite['invited_by'] + invited_on = invite['invited_on'] + expires_on = invite['expires_on'] + status = invite['status'] + print '\t', organization_id, status, invited_member_id, invited_member_email, organization_name, invited_by, invited_on, expires_on + print '' + + print 'BILLING:' + # grab the user billing profile info + try: + profile = cf.user.billing.profile.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/user.billing.profile.get %d %s - api call failed' % (e, e)) + profile_id = profile['id'] + profile_first = profile['first_name'] + profile_last = profile['last_name'] + profile_company = profile['company'] + if profile_company is None: + profile_company = '' + + if profile['payment_email'] != '': + payment_email = profile['payment_email'] + card_number = None + card_expiry_year = None + card_expiry_month = None + else: + payment_email = None + card_number = profile['card_number'] + card_expiry_year = profile['card_expiry_year'] + card_expiry_month = profile['card_expiry_month'] + + if payment_email is not None: + print '\t', profile_id, profile_first, profile_last, profile_company, 'PayPal:', payment_email + else: + if card_number is None: + card_number = '---- ---- ----- ----' + if card_expiry_year is not None and card_expiry_month is not None: + card_expiry = card_expiry_month + '/' + card_expiry_year + else: + card_expiry = '--/--' + print '\t', profile_id, profile_first, profile_last, profile_company, 'CC:', card_number, card_expiry + + print '' + + print 'BILLING HISTORY:' + # grab the user billing history info + try: + history = cf.user.billing.history.get() + except CloudFlare.CloudFlareAPIError as e: + exit('/user.billing.history.get %d %s - api call failed' % (e, e)) + if len(history) == 0: + print '\tNo billing history' + for h in sorted(history, key=lambda v: v['occurred_at']): + history_id = h['id'] + history_type = h['type'] + history_action = h['action'] + history_occurred_at = h['occurred_at'] + history_amount = h['amount'] + history_currency = h['currency'] + history_description = h['description'] + print '\t', history_id, history_type, history_action, history_occurred_at, history_amount, history_currency, history_description + + print '' + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/examples/example-zones.py b/examples/example-zones.py new file mode 100755 index 0000000..2b55fb1 --- /dev/null +++ b/examples/example-zones.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +import re + +def main(): + # Grab the first argument, if there is one + try: + zone_name = sys.argv[1] + params = {'name':zone_name,'per_page':1} + except: + params = {'per_page':50} + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + zones = cf.zones.get(params=params) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones.get - %s - api call failed' % (e)) + + # there should only be one zone + for zone in sorted(zones, key=lambda v: v['name']): + zone_name = zone['name'] + zone_id = zone['id'] + if 'email' in zone['owner']: + zone_owner = zone['owner']['email'] + else: + zone_owner = '"' + zone['owner']['name'] + '"' + zone_plan = zone['plan']['name'] + + try: + dns_records = cf.zones.dns_records.get(zone_id) + except CloudFlare.CloudFlareAPIError as e: + exit('/zones/dns_records %d %s - api call failed' % (e, e)) + + print zone_id, zone_name, zone_owner, zone_plan + + prog = re.compile('\.*'+zone_name+'$') + for dns_record in sorted(dns_records, key=lambda v: prog.sub('', v['name']) + '_' + v['type']): + r_name = dns_record['name'] + r_type = dns_record['type'] + r_value = dns_record['content'] + r_ttl = dns_record['ttl'] + r_id = dns_record['id'] + print '\t%s %60s %6d %-5s %s' % (r_id, r_name, r_ttl, r_type, r_value) + + print '' + + exit(0) + +if __name__ == '__main__': + main() + diff --git a/setup.py b/setup.py index e8b1390..4ca90b2 100755 --- a/setup.py +++ b/setup.py @@ -1,13 +1,20 @@ +#!/usr/bin/env python + from setuptools import setup, find_packages setup( name='python-cloudflare-v4', - version='1.0', + version='1.1', description='Python wrapper for the CloudFlare v4 API', - author='gnowxilef', - author_email='felix@fawong.com', - url='http://github.com/python-cloudflare/python-cloudflare-v4', - packages=find_packages() - ) + author='gnowxilef,Martin J. Levy', + author_email='felix@fawong.com,mahtin@mahin.com', + url='https://github.com/mahtin/python-cloudflare-v4', + packages=['cli4']+find_packages(), + entry_points={ + 'console_scripts': [ + 'cli4 = cli4.__main__:main' + ] + } +) -package_dir = {'cloudflare_v4': 'lib'} +package_dir = {'CloudFlare': 'lib'} diff --git a/tests/test1.py b/tests/test1.py new file mode 100755 index 0000000..4e70dce --- /dev/null +++ b/tests/test1.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +import pytest + +def test_ips(): + cf = CloudFlare.CloudFlare() + zones = cf.ips.get() + assert zones +