From 4671c0ee52b95eded88b591008c0b2c7a1bb35a1 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Sun, 23 Jun 2013 21:20:07 +0200 Subject: [PATCH 01/10] Initial commit --- smartdc/__init__.py | 2 + smartdc/datacenter.py | 7 +- smartdc/network.py | 221 ++++++++++++++++++++++++++++++++++++++++++ smartdc/tef.py | 130 +++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 smartdc/network.py create mode 100644 smartdc/tef.py diff --git a/smartdc/__init__.py b/smartdc/__init__.py index 4ccbc5a..27d7b55 100644 --- a/smartdc/__init__.py +++ b/smartdc/__init__.py @@ -1,6 +1,8 @@ from .datacenter import * from .machine import * from .legacy import LegacyDataCenter +from .network import * +from .tef import TefDataCenter from ._version import get_versions __version__ = get_versions()['version'] diff --git a/smartdc/datacenter.py b/smartdc/datacenter.py index e8aef6e..8472ea4 100644 --- a/smartdc/datacenter.py +++ b/smartdc/datacenter.py @@ -246,7 +246,7 @@ def request(self, method, path, headers=None, data=None, **kwargs): return self.request(method, path, headers=headers, data=data, **kwargs) if 400 <= resp.status_code < 499: - if resp.content: + if resp.content and self.verbose: print(resp.content, file=sys.stderr) resp.raise_for_status() if resp.content: @@ -764,7 +764,8 @@ def create_machine(self, name=None, package=None, dataset=None, params['networks'] = [networks] j, r = self.request('POST', 'machines', data=params) if r.status_code >= 400: - print(j, file=sys.stderr) + if self.verbose: + print(j, file=sys.stderr) r.raise_for_status() return Machine(datacenter=self, data=j) @@ -884,4 +885,4 @@ def image(self, identifier): return j - \ No newline at end of file + diff --git a/smartdc/network.py b/smartdc/network.py new file mode 100644 index 0000000..2102825 --- /dev/null +++ b/smartdc/network.py @@ -0,0 +1,221 @@ +import requests +import time +import uuid + +__all__ = ['Network'] + +class Network(object): + """ + A local proxy representing the state of a remote NetworkAPI subnet. + + A :py:class:`smartdc.network.Network` object is intended to be a + convenient container for methods and data relevant to a remotely running + subnet managed by NetworkAPI. A :py:class:`smartdc.network.Network` is + tied to a :py:class:`smartdc.tef.TefDataCenter` object, and makes all + its requests via that interface. It does not attempt to manage the state + cache in most cases, instead requiring the user to explicitly update with + a :py:meth:`refresh` call. + """ + def __init__(self, datacenter, network_id=None, data=None): + """ + :param datacenter: datacenter that contains this network + :type datacenter: :py:class:`smartdc.tef.TefDataCenter` + + :param network_id: unique ID of the network + :type network_id: :py:class:`basestring` + + :param data: raw data for instantiation + :type data: :py:class:`dict` + + Typically, a :py:class:`smartdc.network.Network` object is + instantiated automatically by a + :py:class:`smartdc.tef.TefDataCenter` object, but a user may + instantiate one with a minimum of a `datacenter` parameter and a + unique ID according to the network. The object then pulls in the + network data from the datacenter API. If `data` is passed in to + instantiate, then ingest the dict and populate internal values from + that. + + All of the following attributes are read-only: + + :var name: human-readable label for the network + :var id: identifier for the network + :var subnet: private subnet (CIDR) + :var resolver_ips: :py:class:`list` of DNS resolver IPs + :var private_gw_ip: private IP of the subnet gateway (:py:class:`basestring`) + :var public_gw_ip: public IP of the subnet gateway (:py:class:`basestring`) + :var status: last-known state of the network (:py:class:`basestring`) + """ + self.id = network_id or data.pop('id') + self.datacenter = datacenter + """the :py:class:`smartdc.tef.TefDataCenter` object that holds + this machine""" + if not data: + data = self.datacenter.raw_network_data(self.id) + self._save(data) + + def __str__(self): + """ + Represents the Network by its unique ID as a string. + """ + return self.id + + def __repr__(self): + """ + Presents a readable representation. + """ + if self.datacenter: + dc = str(self.datacenter) + else: + dc = '' + return '<{module}.{cls}: {name} in {dc}'.format( + module=self.__module__, cls=self.__class__.__name__, + name=self.name, dc=dc) + + def __eq__(self, other): + if isinstance(other, dict): + return self.id == other.get('id') + elif isinstance(other, Network): + return self.id == other.id + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return uuid.UUID(self.id).int + + def _save(self, data): + """ + Take the data from a dict and commit them to appropriate attributes. + """ + self.name = data.get('name') + self.subnet = data.get('subnet') + self.resolver_ips = data.get('resolver_ips') + self.private_gw_ip = data.get('private_gw_ip') + self.public_gw_ip = data.get('public_gw_ip') + self.status = data.get('status') + + @property + def path(self): + """ + Convenience property to insert the id into a relative path for + requests. + """ + return 'networks/{id}'.format(id=self.id) + + @classmethod + def create_in_datacenter(cls, datacenter, name, subnet, **kwargs): + """ + :: + + POST /:login/networks + + Class method, provided as a convenience. + + :param dataceter: datacenter for creating the network + :type datacenter: :py:class:`smartdc.tef.TefDataCenter` + + Provision a network in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an + instantiated :py:class:`smartdc.network.Network` object. + + 'datacenter', 'name' and 'subnet' are required arguments. + The rest of them are passed to the datacenter object as with + :py:meth:`smartdc.tef.TefDataCenter.create_network`. + """ + return datacenter.create_network(name, subnet, **kwargs) + + def refresh(self): + """ + :: + + GET /:login/networks/:id + + Fetch the existing status and values for the + :py:class:`smartdc.network.Network` from the datacenter + and commit the values locally. + """ + try: + self._save(self.datacenter.raw_network_data(self.id)) + except requests.exceptions.HTTPError, err: + # if we try to POST a duplicated subnet, it gets deleted without + # further notice instead of getting a 509 error immediately + if err.message[:4] == '404 ': + self.status = 'error' + else: + raise + + def status(self): + """ + :: + + GET /:login/networks/:id + + :Returns: the current network status + :rtype: :py:class:`basestring` + + Refresh the network's information by fetching it remotely, then + returning the :py:attr:`state` as a string. + """ + self.refresh() + return self.status + + def delete(self): + """ + :: + + DELETE /:login/machines/:id + + Initiate deletion of an empty network. + """ + j, r = self.datacenter.request('DELETE', self.path) + r.raise_for_status() + + def poll_until(self, status, interval=2): + """ + :: + + GET /:login/networks/:id + + :param status: target status + :type status: :py:class:`basestring` + + :param interal: pause in seconds between polls + :type interval: :py:class:`int` + + Convenience method that continuously polls the current state of the + machine remotely, and returns until the named `status` argument is + reached. The default wait `interval` between requests is 2 seconds, + but it may be changed. + + .. Note:: If the next status is wrongly identified, this method may + loop forever. + """ + while self.status() != status: + time.sleep(interval) + + def poll_while(self, status, interval=2): + """ + :: + + GET /:login/networks/:id + + :param status: (assumed) current status + :type status: :py:class:`basestring` + + :param interval: pause in seconds between polls + :type interval: :py:class:`int` + + Convenience method that continuously polls the current status of the + network remotely, and returns while the network has the named `status` + argument. Once the status changes, the method returns. The default wait + `interval` between requests is 2 seconds, but it may be changed. + + .. Note:: If a status transition has not correctly been triggered, this + method may loop forever. + """ + while self.status() == status: + time.sleep(interval) + diff --git a/smartdc/tef.py b/smartdc/tef.py new file mode 100644 index 0000000..735e417 --- /dev/null +++ b/smartdc/tef.py @@ -0,0 +1,130 @@ +from __future__ import print_function +from .legacy import LegacyDataCenter +from .network import Network + +import re + +class TefDataCenter(LegacyDataCenter): + """ + This class provides support for the network extensiones present + on TEF datacenters to the legacy (~6.5) SmartDataCenter API. + + More information: https://api-eu-lon-1.instantservers.telefonica.com/docs/networkapi_docs.html + """ + + def create_network(self, name, subnet, resolver_ips=None): + """ + :: + + POST /:login/networks + + Provision a machine in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated + :py:class:`smartdc.network.Network` object. + + :param name: a human-readable label for the machine + :type name: :py:class:`basestring`, up to 32 letters, digits and + hyphens. This parameter value is required. + + :param subnet: a private :type subnet: :py:class:`basestring` (CDR), + containing a base IP address plus a mask, which must be in the + range from /22 to /27. This parameter value is required. + + :param resolver_ips: list of DNS resolver IPs to be used by the + subnet. If not supplied, the default IPs ("8.8.8.8" and "4.4.4.4") + will be used. + :type resolver_ips: :py:class:`list` + + :rtype: :py:class:`smartdc.network.Network' + """ + params = {} + assert re.match(r'[a-zA-Z0-9-]{1,32}', name), "Illegal name" + params['name'] = name + assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+', + subnet), "Illegal subnet" + params['subnet'] = subnet + if resolver_ips: + if isinstance(resolver_ips, list): + params['resolver_ips'] = resolver_ips + j, r = self.request('POST', 'networks', data=params) + if r.status_code >= 400: + print(j, file=sys.stderr) + r.raise_for_status() + + if isinstance(j, basestring): j = eval(j) #BUGBUGBUG + + return Network(datacenter=self, data=j) + + def raw_network_data(self, network_id): + """ + :: + + GET /:login/networks/:network + + :param network_id: identifier for the network + :type network_id: :py:class:`basestring` or :py:class:`dict` + + :rtype: :py:class:`dict` + + Primarily used internally to get a raw dict for a single network. + """ + params = {} + if isinstance(network_id, dict): + network_id = network_id['id'] + j, r = self.request('GET', 'networks/' + str(network_id), + params=params) + + if isinstance(j, basestring): j = eval(j) #BUGBUGBUG + + return j + + def networks(self, search=None, fields=('name,')): + """ + :: + + GET /:login/networks + + :param search: optionally filter (locally) with a regular expression + search on the listed fields + :type search: :py:class:`basestring` that compiles as a regular + expression + + :param fields: filter on the listed fields (defaulting to + ``name``) + :type fields: :py:class:`list` of :py:class:`basestring`\s + + :Returns: network available in this datacenter + :rtype: :py:class:`list` of :py:class:`dict`\s + """ + + j, _ = self.request('GET', 'networks') + + if isinstance(j, basestring): j = eval(j) #BUGBUGBUG + + if search: + return list(search_dicts(j, search, fields)) + else: + return j + + def network(self, identifier): + """ + :: + + GET /:login/networks/:id + + :param identifier: match on the listed network identifier + :type identifier: :py:class:`basestring` or :py:class:`dict` + + :Returns: characteristics of the requested network + :rtype: :py:class:`dict` + + Either a string or a dictionary with an ``id`` key may be passed in. + """ + + if isinstance(identifier, dict): + identifier = identifier.get('id') + j, _ = self.request('GET', 'networks/' + str(identifier)) + + if isinstance(j, basestring): j = eval(j) #BUGBUGBUG + + return j From 50462f469c7b6c5db01c461540a65d864ed977cb Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Sun, 23 Jun 2013 22:05:06 +0200 Subject: [PATCH 02/10] Add the 'create_machine' proxy, plus remove unnecessary error checking. --- smartdc/network.py | 11 +----- smartdc/tef.py | 96 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/smartdc/network.py b/smartdc/network.py index 2102825..dcf5abc 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -137,15 +137,8 @@ def refresh(self): :py:class:`smartdc.network.Network` from the datacenter and commit the values locally. """ - try: - self._save(self.datacenter.raw_network_data(self.id)) - except requests.exceptions.HTTPError, err: - # if we try to POST a duplicated subnet, it gets deleted without - # further notice instead of getting a 509 error immediately - if err.message[:4] == '404 ': - self.status = 'error' - else: - raise + data = self.datacenter.raw_network_data(self.id) + self._save(data) def status(self): """ diff --git a/smartdc/tef.py b/smartdc/tef.py index 735e417..27db749 100644 --- a/smartdc/tef.py +++ b/smartdc/tef.py @@ -1,8 +1,10 @@ from __future__ import print_function from .legacy import LegacyDataCenter from .network import Network +from .machine import Machine import re +import sys class TefDataCenter(LegacyDataCenter): """ @@ -27,7 +29,7 @@ def create_network(self, name, subnet, resolver_ips=None): hyphens. This parameter value is required. :param subnet: a private :type subnet: :py:class:`basestring` (CDR), - containing a base IP address plus a mask, which must be in the + containing a base IP address plus a mask, which must be in the range from /22 to /27. This parameter value is required. :param resolver_ips: list of DNS resolver IPs to be used by the @@ -128,3 +130,95 @@ def network(self, identifier): if isinstance(j, basestring): j = eval(j) #BUGBUGBUG return j + + def create_machine(self, name=None, package=None, dataset=None, + metadata=None, tags=None, boot_script=None, credentials=False, + image=None, network_id=None): + """ + :: + + POST /:login/machines + + Provision a machine in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated + :py:class:`smartdc.machine.Machine` object. All of the parameter + values are optional, as they are assigned default values by the + datacenter's API itself. + + :param name: a human-readable label for the machine + :type name: :py:class:`basestring` + + :param package: cluster of resource values identified by name + :type package: :py:class:`basestring` or :py:class:`dict` + + :param image: an identifier for the base operating system image + (formerly a ``dataset``) + :type image: :py:class:`basestring` or :py:class:`dict` + + :param dataset: base operating system image identified by a globally + unique ID or URN (deprecated) + :type dataset: :py:class:`basestring` or :py:class:`dict` + + :param metadata: keys & values with arbitrary supplementary + details for the machine, accessible from the machine itself + :type metadata: :py:class:`dict` + + :param tags: keys & values with arbitrary supplementary + identifying information for filtering when querying for machines + :type tags: :py:class:`dict` + + :param network_id: network id where this machine will belong to; if + omitted, the machine will have a public IP address. PLEASE note + that if this parameter is specified, then ``name``, ``package`` + and ``dataset`` are compulsory. + :type network_id: :py:class:`basestring` + + :param boot_script: path to a file to upload for execution on boot + :type boot_script: :py:class:`basestring` as file path + + :rtype: :py:class:`smartdc.machine.Machine` + + If `package`, `image`, or `dataset` are passed a :py:class:`dict` containing a + `name` key (in the case of `package`) or an `id` key (in the case of + `image` or `dataset`), it passes the corresponding value. The server API + appears to resolve incomplete or ambiguous dataset URNs with the + highest version number. + """ + params = {} + if name: + assert re.match(r'[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$', + name), "Illegal name" + params['name'] = name + if package: + if isinstance(package, dict): + package = package['name'] + params['package'] = package + if image: + if isinstance(image, dict): + image = image['id'] + params['image'] = image + if dataset and not image: + if isinstance(dataset, dict): + dataset = dataset.get('id', dataset['urn']) + params['dataset'] = dataset + if metadata: + for k, v in metadata.items(): + params['metadata.' + str(k)] = v + if tags: + for k, v in tags.items(): + params['tag.' + str(k)] = v + if boot_script: + with open(boot_script) as f: + params['metadata.user-script'] = f.read() + if network_id: + if isinstance(network_id, basestring): + params['network_id'] = network_id + j, r = self.request('POST', 'machines', data=params) + if r.status_code >= 400: + if self.verbose: + print(j, file=sys.stderr) + r.raise_for_status() + + if isinstance(j, basestring): j = eval(j) #BUGBUGBUG + + return Machine(datacenter=self, data=j) From 688ddd7836f2427254047704159c43c208e7e156 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Mon, 24 Jun 2013 21:12:49 +0200 Subject: [PATCH 03/10] Add set_outbound/get_outbound to the Network proxy --- smartdc/network.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/smartdc/network.py b/smartdc/network.py index dcf5abc..433b879 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -1,6 +1,7 @@ import requests import time import uuid +import json __all__ = ['Network'] @@ -95,7 +96,7 @@ def _save(self, data): self.resolver_ips = data.get('resolver_ips') self.private_gw_ip = data.get('private_gw_ip') self.public_gw_ip = data.get('public_gw_ip') - self.status = data.get('status') + self.state = data.get('status') @property def path(self): @@ -153,7 +154,7 @@ def status(self): returning the :py:attr:`state` as a string. """ self.refresh() - return self.status + return self.state def delete(self): """ @@ -212,3 +213,40 @@ def poll_while(self, status, interval=2): while self.status() == status: time.sleep(interval) + def set_outbound(self, enabled): + """ + :: + + PUT /:login/networks/:id/outbound + + :param enabled: new status for the curret network outbound PAT (Port + Address Translation). + :type enabled: :py:class:`bool` + + :Returns: the updated network outbound PAT (Port Address Translation) + status. + :rtype: :py:class:`bool` + """ + assert isinstance(enabled, bool), "Illegal status" + j, _ = self.datacenter.request('PUT', self.path + '/outbound', + data={ 'enabled': enabled }) + + if isinstance(j, basestring): j = json.loads(j) #BUGBUGBUG + + return j['enabled'] + + def get_outbound(self): + """ + :: + + GET /:login/networks/:id/outbound + + :Returns: the current network outbound PAT (Port Address Translation) + status. + :rtype: :py:class:`bool` + """ + j, _ = self.datacenter.request('GET', self.path + '/outbound') + + if isinstance(j, basestring): j = json.loads(j) #BUGBUGBUG + + return j['enabled'] From f0f0bc241dc0db241c2d4e64647fdcaa2b1ee7e2 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Mon, 24 Jun 2013 21:15:11 +0200 Subject: [PATCH 04/10] New doc stubs --- docs/network.rst | 5 +++++ docs/tef.rst | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 docs/network.rst create mode 100644 docs/tef.rst diff --git a/docs/network.rst b/docs/network.rst new file mode 100644 index 0000000..b94378a --- /dev/null +++ b/docs/network.rst @@ -0,0 +1,5 @@ +:mod:`smartdc.network` Module +============================= + +.. autoclass:: smartdc.network.Network + diff --git a/docs/tef.rst b/docs/tef.rst new file mode 100644 index 0000000..29f8934 --- /dev/null +++ b/docs/tef.rst @@ -0,0 +1,5 @@ +:mod:`smartdc.tef` Module +============================ + +.. autoclass:: smartdc.tef.TefDataCenter + From a3eeece7236923239b3ead321e2143f64c117102 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Tue, 25 Jun 2013 08:01:36 +0200 Subject: [PATCH 05/10] Handel Content-Type 'application/json; encoding=...'; cosmetics --- smartdc/datacenter.py | 8 +++++--- smartdc/network.py | 8 +------- smartdc/tef.py | 21 ++------------------- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/smartdc/datacenter.py b/smartdc/datacenter.py index 8472ea4..04a712c 100644 --- a/smartdc/datacenter.py +++ b/smartdc/datacenter.py @@ -154,7 +154,7 @@ def __repr__(self): user_string = '<{0}> '.format(self.login) else: user_string = '' - return '<{module}.{cls}: {name}at <{loc}>>'.format( + return '<{module}.{cls}: {name} at <{loc}>>'.format( module=self.__module__, cls=self.__class__.__name__, name=user_string, loc=self.location) @@ -250,8 +250,10 @@ def request(self, method, path, headers=None, data=None, **kwargs): print(resp.content, file=sys.stderr) resp.raise_for_status() if resp.content: - if resp.headers['content-type'] == 'application/json': - return (json.loads(resp.content), resp) + ctype = re.match('application/json(; +charset *= *([a-zA-z0-9-_]+))? *', + resp.headers['content-type']) + if ctype: + return (json.loads(resp.content, ctype.groups('utf-8')[1]), resp) else: return (resp.content, resp) else: diff --git a/smartdc/network.py b/smartdc/network.py index 433b879..2096003 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -69,7 +69,7 @@ def __repr__(self): dc = str(self.datacenter) else: dc = '' - return '<{module}.{cls}: {name} in {dc}'.format( + return '<{module}.{cls}: <{name}> in {dc}>'.format( module=self.__module__, cls=self.__class__.__name__, name=self.name, dc=dc) @@ -230,9 +230,6 @@ def set_outbound(self, enabled): assert isinstance(enabled, bool), "Illegal status" j, _ = self.datacenter.request('PUT', self.path + '/outbound', data={ 'enabled': enabled }) - - if isinstance(j, basestring): j = json.loads(j) #BUGBUGBUG - return j['enabled'] def get_outbound(self): @@ -246,7 +243,4 @@ def get_outbound(self): :rtype: :py:class:`bool` """ j, _ = self.datacenter.request('GET', self.path + '/outbound') - - if isinstance(j, basestring): j = json.loads(j) #BUGBUGBUG - return j['enabled'] diff --git a/smartdc/tef.py b/smartdc/tef.py index 27db749..95bbf6f 100644 --- a/smartdc/tef.py +++ b/smartdc/tef.py @@ -52,9 +52,6 @@ def create_network(self, name, subnet, resolver_ips=None): if r.status_code >= 400: print(j, file=sys.stderr) r.raise_for_status() - - if isinstance(j, basestring): j = eval(j) #BUGBUGBUG - return Network(datacenter=self, data=j) def raw_network_data(self, network_id): @@ -75,9 +72,6 @@ def raw_network_data(self, network_id): network_id = network_id['id'] j, r = self.request('GET', 'networks/' + str(network_id), params=params) - - if isinstance(j, basestring): j = eval(j) #BUGBUGBUG - return j def networks(self, search=None, fields=('name,')): @@ -98,15 +92,10 @@ def networks(self, search=None, fields=('name,')): :Returns: network available in this datacenter :rtype: :py:class:`list` of :py:class:`dict`\s """ - j, _ = self.request('GET', 'networks') - - if isinstance(j, basestring): j = eval(j) #BUGBUGBUG - if search: - return list(search_dicts(j, search, fields)) - else: - return j + j = list(search_dicts(j, search, fields)) + return [Network(datacenter=self, data=m) for m in j] def network(self, identifier): """ @@ -126,9 +115,6 @@ def network(self, identifier): if isinstance(identifier, dict): identifier = identifier.get('id') j, _ = self.request('GET', 'networks/' + str(identifier)) - - if isinstance(j, basestring): j = eval(j) #BUGBUGBUG - return j def create_machine(self, name=None, package=None, dataset=None, @@ -218,7 +204,4 @@ def create_machine(self, name=None, package=None, dataset=None, if self.verbose: print(j, file=sys.stderr) r.raise_for_status() - - if isinstance(j, basestring): j = eval(j) #BUGBUGBUG - return Machine(datacenter=self, data=j) From fd14de1c60db797fdaf40119728b0f8cb2daa420 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Tue, 25 Jun 2013 08:09:53 +0200 Subject: [PATCH 06/10] Tabs to spaces --- smartdc/network.py | 477 +++++++++++++++++++++++---------------------- smartdc/tef.py | 396 ++++++++++++++++++------------------- 2 files changed, 437 insertions(+), 436 deletions(-) diff --git a/smartdc/network.py b/smartdc/network.py index 2096003..c702349 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -6,241 +6,242 @@ __all__ = ['Network'] class Network(object): - """ - A local proxy representing the state of a remote NetworkAPI subnet. - - A :py:class:`smartdc.network.Network` object is intended to be a - convenient container for methods and data relevant to a remotely running - subnet managed by NetworkAPI. A :py:class:`smartdc.network.Network` is - tied to a :py:class:`smartdc.tef.TefDataCenter` object, and makes all - its requests via that interface. It does not attempt to manage the state - cache in most cases, instead requiring the user to explicitly update with - a :py:meth:`refresh` call. - """ - def __init__(self, datacenter, network_id=None, data=None): - """ - :param datacenter: datacenter that contains this network - :type datacenter: :py:class:`smartdc.tef.TefDataCenter` - - :param network_id: unique ID of the network - :type network_id: :py:class:`basestring` - - :param data: raw data for instantiation - :type data: :py:class:`dict` - - Typically, a :py:class:`smartdc.network.Network` object is - instantiated automatically by a - :py:class:`smartdc.tef.TefDataCenter` object, but a user may - instantiate one with a minimum of a `datacenter` parameter and a - unique ID according to the network. The object then pulls in the - network data from the datacenter API. If `data` is passed in to - instantiate, then ingest the dict and populate internal values from - that. - - All of the following attributes are read-only: - - :var name: human-readable label for the network - :var id: identifier for the network - :var subnet: private subnet (CIDR) - :var resolver_ips: :py:class:`list` of DNS resolver IPs - :var private_gw_ip: private IP of the subnet gateway (:py:class:`basestring`) - :var public_gw_ip: public IP of the subnet gateway (:py:class:`basestring`) - :var status: last-known state of the network (:py:class:`basestring`) - """ - self.id = network_id or data.pop('id') - self.datacenter = datacenter - """the :py:class:`smartdc.tef.TefDataCenter` object that holds - this machine""" - if not data: - data = self.datacenter.raw_network_data(self.id) - self._save(data) - - def __str__(self): - """ - Represents the Network by its unique ID as a string. - """ - return self.id - - def __repr__(self): - """ - Presents a readable representation. - """ - if self.datacenter: - dc = str(self.datacenter) - else: - dc = '' - return '<{module}.{cls}: <{name}> in {dc}>'.format( - module=self.__module__, cls=self.__class__.__name__, - name=self.name, dc=dc) - - def __eq__(self, other): - if isinstance(other, dict): - return self.id == other.get('id') - elif isinstance(other, Network): - return self.id == other.id - else: - return False - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return uuid.UUID(self.id).int - - def _save(self, data): - """ - Take the data from a dict and commit them to appropriate attributes. - """ - self.name = data.get('name') - self.subnet = data.get('subnet') - self.resolver_ips = data.get('resolver_ips') - self.private_gw_ip = data.get('private_gw_ip') - self.public_gw_ip = data.get('public_gw_ip') - self.state = data.get('status') - - @property - def path(self): - """ - Convenience property to insert the id into a relative path for - requests. - """ - return 'networks/{id}'.format(id=self.id) - - @classmethod - def create_in_datacenter(cls, datacenter, name, subnet, **kwargs): - """ - :: - - POST /:login/networks - - Class method, provided as a convenience. - - :param dataceter: datacenter for creating the network - :type datacenter: :py:class:`smartdc.tef.TefDataCenter` - - Provision a network in the current - :py:class:`smartdc.tef.TefDataCenter`, returning an - instantiated :py:class:`smartdc.network.Network` object. - - 'datacenter', 'name' and 'subnet' are required arguments. - The rest of them are passed to the datacenter object as with - :py:meth:`smartdc.tef.TefDataCenter.create_network`. - """ - return datacenter.create_network(name, subnet, **kwargs) - - def refresh(self): - """ - :: - - GET /:login/networks/:id - - Fetch the existing status and values for the - :py:class:`smartdc.network.Network` from the datacenter - and commit the values locally. - """ - data = self.datacenter.raw_network_data(self.id) - self._save(data) - - def status(self): - """ - :: - - GET /:login/networks/:id - - :Returns: the current network status - :rtype: :py:class:`basestring` - - Refresh the network's information by fetching it remotely, then - returning the :py:attr:`state` as a string. - """ - self.refresh() - return self.state - - def delete(self): - """ - :: - - DELETE /:login/machines/:id - - Initiate deletion of an empty network. - """ - j, r = self.datacenter.request('DELETE', self.path) - r.raise_for_status() - - def poll_until(self, status, interval=2): - """ - :: - - GET /:login/networks/:id - - :param status: target status - :type status: :py:class:`basestring` - - :param interal: pause in seconds between polls - :type interval: :py:class:`int` - - Convenience method that continuously polls the current state of the - machine remotely, and returns until the named `status` argument is - reached. The default wait `interval` between requests is 2 seconds, - but it may be changed. - - .. Note:: If the next status is wrongly identified, this method may - loop forever. - """ - while self.status() != status: - time.sleep(interval) - - def poll_while(self, status, interval=2): - """ - :: - - GET /:login/networks/:id - - :param status: (assumed) current status - :type status: :py:class:`basestring` - - :param interval: pause in seconds between polls - :type interval: :py:class:`int` - - Convenience method that continuously polls the current status of the - network remotely, and returns while the network has the named `status` - argument. Once the status changes, the method returns. The default wait - `interval` between requests is 2 seconds, but it may be changed. - - .. Note:: If a status transition has not correctly been triggered, this - method may loop forever. - """ - while self.status() == status: - time.sleep(interval) - - def set_outbound(self, enabled): - """ - :: - - PUT /:login/networks/:id/outbound - - :param enabled: new status for the curret network outbound PAT (Port - Address Translation). - :type enabled: :py:class:`bool` - - :Returns: the updated network outbound PAT (Port Address Translation) - status. - :rtype: :py:class:`bool` - """ - assert isinstance(enabled, bool), "Illegal status" - j, _ = self.datacenter.request('PUT', self.path + '/outbound', - data={ 'enabled': enabled }) - return j['enabled'] - - def get_outbound(self): - """ - :: - - GET /:login/networks/:id/outbound - - :Returns: the current network outbound PAT (Port Address Translation) - status. - :rtype: :py:class:`bool` - """ - j, _ = self.datacenter.request('GET', self.path + '/outbound') - return j['enabled'] + """ + A local proxy representing the state of a remote NetworkAPI subnet. + + A :py:class:`smartdc.network.Network` object is intended to be a + convenient container for methods and data relevant to a remotely running + subnet managed by NetworkAPI. A :py:class:`smartdc.network.Network` is + tied to a :py:class:`smartdc.tef.TefDataCenter` object, and makes all + its requests via that interface. It does not attempt to manage the state + cache in most cases, instead requiring the user to explicitly update with + a :py:meth:`refresh` call. + """ + def __init__(self, datacenter, network_id=None, data=None): + """ + :param datacenter: datacenter that contains this network + :type datacenter: :py:class:`smartdc.tef.TefDataCenter` + + :param network_id: unique ID of the network + :type network_id: :py:class:`basestring` + + :param data: raw data for instantiation + :type data: :py:class:`dict` + + Typically, a :py:class:`smartdc.network.Network` object is + instantiated automatically by a + :py:class:`smartdc.tef.TefDataCenter` object, but a user may + instantiate one with a minimum of a `datacenter` parameter and a + unique ID according to the network. The object then pulls in the + network data from the datacenter API. If `data` is passed in to + instantiate, then ingest the dict and populate internal values from + that. + + All of the following attributes are read-only: + + :var name: human-readable label for the network + :var id: identifier for the network + :var subnet: private subnet (CIDR) + :var resolver_ips: :py:class:`list` of DNS resolver IPs + :var private_gw_ip: private IP of the subnet gateway (:py:class:`basestring`) + :var public_gw_ip: public IP of the subnet gateway (:py:class:`basestring`) + :var status: last-known state of the network (:py:class:`basestring`) + """ + self.id = network_id or data.pop('id') + self.datacenter = datacenter + """the :py:class:`smartdc.tef.TefDataCenter` object that holds + this machine""" + if not data: + data = self.datacenter.raw_network_data(self.id) + self._save(data) + + def __str__(self): + """ + Represents the Network by its unique ID as a string. + """ + return self.id + + def __repr__(self): + """ + Presents a readable representation. + """ + if self.datacenter: + dc = str(self.datacenter) + else: + dc = '' + return '<{module}.{cls}: <{name}> in {dc}>'.format( + module=self.__module__, cls=self.__class__.__name__, + name=self.name, dc=dc) + + def __eq__(self, other): + if isinstance(other, dict): + return self.id == other.get('id') + elif isinstance(other, Network): + return self.id == other.id + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return uuid.UUID(self.id).int + + def _save(self, data): + """ + Take the data from a dict and commit them to appropriate attributes. + """ + self.name = data.get('name') + self.subnet = data.get('subnet') + self.resolver_ips = data.get('resolver_ips') + self.private_gw_ip = data.get('private_gw_ip') + self.public_gw_ip = data.get('public_gw_ip') + self.state = data.get('status') + + @property + def path(self): + """ + Convenience property to insert the id into a relative path for + requests. + """ + return 'networks/{id}'.format(id=self.id) + + @classmethod + def create_in_datacenter(cls, datacenter, name, subnet, **kwargs): + """ + :: + + POST /:login/networks + + Class method, provided as a convenience. + + :param dataceter: datacenter for creating the network + :type datacenter: :py:class:`smartdc.tef.TefDataCenter` + + Provision a network in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an + instantiated :py:class:`smartdc.network.Network` object. + + 'datacenter', 'name' and 'subnet' are required arguments. + The rest of them are passed to the datacenter object as with + :py:meth:`smartdc.tef.TefDataCenter.create_network`. + """ + return datacenter.create_network(name, subnet, **kwargs) + + def refresh(self): + """ + :: + + GET /:login/networks/:id + + Fetch the existing status and values for the + :py:class:`smartdc.network.Network` from the datacenter + and commit the values locally. + """ + data = self.datacenter.raw_network_data(self.id) + self._save(data) + + def status(self): + """ + :: + + GET /:login/networks/:id + + :Returns: the current network status + :rtype: :py:class:`basestring` + + Refresh the network's information by fetching it remotely, then + returning the :py:attr:`state` as a string. + """ + self.refresh() + return self.state + + def delete(self): + """ + :: + + DELETE /:login/machines/:id + + Initiate deletion of an empty network. + """ + j, r = self.datacenter.request('DELETE', self.path) + r.raise_for_status() + + def poll_until(self, status, interval=2): + """ + :: + + GET /:login/networks/:id + + :param status: target status + :type status: :py:class:`basestring` + + :param interal: pause in seconds between polls + :type interval: :py:class:`int` + + Convenience method that continuously polls the current state of the + machine remotely, and returns until the named `status` argument is + reached. The default wait `interval` between requests is 2 seconds, + but it may be changed. + + .. Note:: If the next status is wrongly identified, this method may + loop forever. + """ + while self.status() != status: + time.sleep(interval) + + def poll_while(self, status, interval=2): + """ + :: + + GET /:login/networks/:id + + :param status: (assumed) current status + :type status: :py:class:`basestring` + + :param interval: pause in seconds between polls + :type interval: :py:class:`int` + + Convenience method that continuously polls the current status of the + network remotely, and returns while the network has the named `status` + argument. Once the status changes, the method returns. The default wait + `interval` between requests is 2 seconds, but it may be changed. + + .. Note:: If a status transition has not correctly been triggered, this + method may loop forever. + """ + while self.status() == status: + time.sleep(interval) + + def set_outbound(self, enabled): + """ + :: + + PUT /:login/networks/:id/outbound + + :param enabled: new status for the curret network outbound PAT (Port + Address Translation). + :type enabled: :py:class:`bool` + + :Returns: the updated network outbound PAT (Port Address Translation) + status. + :rtype: :py:class:`bool` + """ + assert isinstance(enabled, bool), "Illegal status" + j, _ = self.datacenter.request('PUT', self.path + '/outbound', + data={ 'enabled': enabled }) + return j['enabled'] + + def get_outbound(self): + """ + :: + + GET /:login/networks/:id/outbound + + :Returns: the current network outbound PAT (Port Address Translation) + status. + :rtype: :py:class:`bool` + """ + j, _ = self.datacenter.request('GET', self.path + '/outbound') + return j['enabled'] + diff --git a/smartdc/tef.py b/smartdc/tef.py index 95bbf6f..42684be 100644 --- a/smartdc/tef.py +++ b/smartdc/tef.py @@ -7,201 +7,201 @@ import sys class TefDataCenter(LegacyDataCenter): - """ - This class provides support for the network extensiones present - on TEF datacenters to the legacy (~6.5) SmartDataCenter API. - - More information: https://api-eu-lon-1.instantservers.telefonica.com/docs/networkapi_docs.html - """ - - def create_network(self, name, subnet, resolver_ips=None): - """ - :: - - POST /:login/networks - - Provision a machine in the current - :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated - :py:class:`smartdc.network.Network` object. - - :param name: a human-readable label for the machine - :type name: :py:class:`basestring`, up to 32 letters, digits and - hyphens. This parameter value is required. - - :param subnet: a private :type subnet: :py:class:`basestring` (CDR), - containing a base IP address plus a mask, which must be in the - range from /22 to /27. This parameter value is required. - - :param resolver_ips: list of DNS resolver IPs to be used by the - subnet. If not supplied, the default IPs ("8.8.8.8" and "4.4.4.4") - will be used. - :type resolver_ips: :py:class:`list` - - :rtype: :py:class:`smartdc.network.Network' - """ - params = {} - assert re.match(r'[a-zA-Z0-9-]{1,32}', name), "Illegal name" - params['name'] = name - assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+', - subnet), "Illegal subnet" - params['subnet'] = subnet - if resolver_ips: - if isinstance(resolver_ips, list): - params['resolver_ips'] = resolver_ips - j, r = self.request('POST', 'networks', data=params) - if r.status_code >= 400: - print(j, file=sys.stderr) - r.raise_for_status() - return Network(datacenter=self, data=j) - - def raw_network_data(self, network_id): - """ - :: - - GET /:login/networks/:network - - :param network_id: identifier for the network - :type network_id: :py:class:`basestring` or :py:class:`dict` - - :rtype: :py:class:`dict` - - Primarily used internally to get a raw dict for a single network. - """ - params = {} - if isinstance(network_id, dict): - network_id = network_id['id'] - j, r = self.request('GET', 'networks/' + str(network_id), - params=params) - return j - - def networks(self, search=None, fields=('name,')): - """ - :: - - GET /:login/networks - - :param search: optionally filter (locally) with a regular expression - search on the listed fields - :type search: :py:class:`basestring` that compiles as a regular - expression - - :param fields: filter on the listed fields (defaulting to - ``name``) - :type fields: :py:class:`list` of :py:class:`basestring`\s - - :Returns: network available in this datacenter - :rtype: :py:class:`list` of :py:class:`dict`\s - """ - j, _ = self.request('GET', 'networks') - if search: - j = list(search_dicts(j, search, fields)) - return [Network(datacenter=self, data=m) for m in j] - - def network(self, identifier): - """ - :: - - GET /:login/networks/:id - - :param identifier: match on the listed network identifier - :type identifier: :py:class:`basestring` or :py:class:`dict` - - :Returns: characteristics of the requested network - :rtype: :py:class:`dict` - - Either a string or a dictionary with an ``id`` key may be passed in. - """ - - if isinstance(identifier, dict): - identifier = identifier.get('id') - j, _ = self.request('GET', 'networks/' + str(identifier)) - return j - - def create_machine(self, name=None, package=None, dataset=None, - metadata=None, tags=None, boot_script=None, credentials=False, - image=None, network_id=None): - """ - :: - - POST /:login/machines - - Provision a machine in the current - :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated - :py:class:`smartdc.machine.Machine` object. All of the parameter - values are optional, as they are assigned default values by the - datacenter's API itself. - - :param name: a human-readable label for the machine - :type name: :py:class:`basestring` - - :param package: cluster of resource values identified by name - :type package: :py:class:`basestring` or :py:class:`dict` - - :param image: an identifier for the base operating system image - (formerly a ``dataset``) - :type image: :py:class:`basestring` or :py:class:`dict` - - :param dataset: base operating system image identified by a globally - unique ID or URN (deprecated) - :type dataset: :py:class:`basestring` or :py:class:`dict` - - :param metadata: keys & values with arbitrary supplementary - details for the machine, accessible from the machine itself - :type metadata: :py:class:`dict` - - :param tags: keys & values with arbitrary supplementary - identifying information for filtering when querying for machines - :type tags: :py:class:`dict` - - :param network_id: network id where this machine will belong to; if - omitted, the machine will have a public IP address. PLEASE note - that if this parameter is specified, then ``name``, ``package`` - and ``dataset`` are compulsory. - :type network_id: :py:class:`basestring` - - :param boot_script: path to a file to upload for execution on boot - :type boot_script: :py:class:`basestring` as file path - - :rtype: :py:class:`smartdc.machine.Machine` - - If `package`, `image`, or `dataset` are passed a :py:class:`dict` containing a - `name` key (in the case of `package`) or an `id` key (in the case of - `image` or `dataset`), it passes the corresponding value. The server API - appears to resolve incomplete or ambiguous dataset URNs with the - highest version number. - """ - params = {} - if name: - assert re.match(r'[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$', - name), "Illegal name" - params['name'] = name - if package: - if isinstance(package, dict): - package = package['name'] - params['package'] = package - if image: - if isinstance(image, dict): - image = image['id'] - params['image'] = image - if dataset and not image: - if isinstance(dataset, dict): - dataset = dataset.get('id', dataset['urn']) - params['dataset'] = dataset - if metadata: - for k, v in metadata.items(): - params['metadata.' + str(k)] = v - if tags: - for k, v in tags.items(): - params['tag.' + str(k)] = v - if boot_script: - with open(boot_script) as f: - params['metadata.user-script'] = f.read() - if network_id: - if isinstance(network_id, basestring): - params['network_id'] = network_id - j, r = self.request('POST', 'machines', data=params) - if r.status_code >= 400: - if self.verbose: - print(j, file=sys.stderr) - r.raise_for_status() - return Machine(datacenter=self, data=j) + """ + This class provides support for the network extensiones present + on TEF datacenters to the legacy (~6.5) SmartDataCenter API. + + More information: https://api-eu-lon-1.instantservers.telefonica.com/docs/networkapi_docs.html + """ + + def create_network(self, name, subnet, resolver_ips=None): + """ + :: + + POST /:login/networks + + Provision a machine in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated + :py:class:`smartdc.network.Network` object. + + :param name: a human-readable label for the machine + :type name: :py:class:`basestring`, up to 32 letters, digits and + hyphens. This parameter value is required. + + :param subnet: a private :type subnet: :py:class:`basestring` (CDR), + containing a base IP address plus a mask, which must be in the + range from /22 to /27. This parameter value is required. + + :param resolver_ips: list of DNS resolver IPs to be used by the + subnet. If not supplied, the default IPs ("8.8.8.8" and "4.4.4.4") + will be used. + :type resolver_ips: :py:class:`list` + + :rtype: :py:class:`smartdc.network.Network' + """ + params = {} + assert re.match(r'[a-zA-Z0-9-]{1,32}', name), "Illegal name" + params['name'] = name + assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+', + subnet), "Illegal subnet" + params['subnet'] = subnet + if resolver_ips: + if isinstance(resolver_ips, list): + params['resolver_ips'] = resolver_ips + j, r = self.request('POST', 'networks', data=params) + if r.status_code >= 400: + print(j, file=sys.stderr) + r.raise_for_status() + return Network(datacenter=self, data=j) + + def raw_network_data(self, network_id): + """ + :: + + GET /:login/networks/:network + + :param network_id: identifier for the network + :type network_id: :py:class:`basestring` or :py:class:`dict` + + :rtype: :py:class:`dict` + + Primarily used internally to get a raw dict for a single network. + """ + params = {} + if isinstance(network_id, dict): + network_id = network_id['id'] + j, r = self.request('GET', 'networks/' + str(network_id), + params=params) + return j + + def networks(self, search=None, fields=('name,')): + """ + :: + + GET /:login/networks + + :param search: optionally filter (locally) with a regular expression + search on the listed fields + :type search: :py:class:`basestring` that compiles as a regular + expression + + :param fields: filter on the listed fields (defaulting to + ``name``) + :type fields: :py:class:`list` of :py:class:`basestring`\s + + :Returns: network available in this datacenter + :rtype: :py:class:`list` of :py:class:`dict`\s + """ + j, _ = self.request('GET', 'networks') + if search: + j = list(search_dicts(j, search, fields)) + return [Network(datacenter=self, data=m) for m in j] + + def network(self, identifier): + """ + :: + + GET /:login/networks/:id + + :param identifier: match on the listed network identifier + :type identifier: :py:class:`basestring` or :py:class:`dict` + + :Returns: characteristics of the requested network + :rtype: :py:class:`dict` + + Either a string or a dictionary with an ``id`` key may be passed in. + """ + + if isinstance(identifier, dict): + identifier = identifier.get('id') + j, _ = self.request('GET', 'networks/' + str(identifier)) + return j + + def create_machine(self, name=None, package=None, dataset=None, + metadata=None, tags=None, boot_script=None, credentials=False, + image=None, network_id=None): + """ + :: + + POST /:login/machines + + Provision a machine in the current + :py:class:`smartdc.tef.TefDataCenter`, returning an instantiated + :py:class:`smartdc.machine.Machine` object. All of the parameter + values are optional, as they are assigned default values by the + datacenter's API itself. + + :param name: a human-readable label for the machine + :type name: :py:class:`basestring` + + :param package: cluster of resource values identified by name + :type package: :py:class:`basestring` or :py:class:`dict` + + :param image: an identifier for the base operating system image + (formerly a ``dataset``) + :type image: :py:class:`basestring` or :py:class:`dict` + + :param dataset: base operating system image identified by a globally + unique ID or URN (deprecated) + :type dataset: :py:class:`basestring` or :py:class:`dict` + + :param metadata: keys & values with arbitrary supplementary + details for the machine, accessible from the machine itself + :type metadata: :py:class:`dict` + + :param tags: keys & values with arbitrary supplementary + identifying information for filtering when querying for machines + :type tags: :py:class:`dict` + + :param network_id: network id where this machine will belong to; if + omitted, the machine will have a public IP address. PLEASE note + that if this parameter is specified, then ``name``, ``package`` + and ``dataset`` are compulsory. + :type network_id: :py:class:`basestring` + + :param boot_script: path to a file to upload for execution on boot + :type boot_script: :py:class:`basestring` as file path + + :rtype: :py:class:`smartdc.machine.Machine` + + If `package`, `image`, or `dataset` are passed a :py:class:`dict` containing a + `name` key (in the case of `package`) or an `id` key (in the case of + `image` or `dataset`), it passes the corresponding value. The server API + appears to resolve incomplete or ambiguous dataset URNs with the + highest version number. + """ + params = {} + if name: + assert re.match(r'[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$', + name), "Illegal name" + params['name'] = name + if package: + if isinstance(package, dict): + package = package['name'] + params['package'] = package + if image: + if isinstance(image, dict): + image = image['id'] + params['image'] = image + if dataset and not image: + if isinstance(dataset, dict): + dataset = dataset.get('id', dataset['urn']) + params['dataset'] = dataset + if metadata: + for k, v in metadata.items(): + params['metadata.' + str(k)] = v + if tags: + for k, v in tags.items(): + params['tag.' + str(k)] = v + if boot_script: + with open(boot_script) as f: + params['metadata.user-script'] = f.read() + if network_id: + if isinstance(network_id, basestring): + params['network_id'] = network_id + j, r = self.request('POST', 'machines', data=params) + if r.status_code >= 400: + if self.verbose: + print(j, file=sys.stderr) + r.raise_for_status() + return Machine(datacenter=self, data=j) From 80805d717c172e086e562f64b7d44bf3453f5aac Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Tue, 25 Jun 2013 08:14:35 +0200 Subject: [PATCH 07/10] Cosmetic --- smartdc/datacenter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartdc/datacenter.py b/smartdc/datacenter.py index 04a712c..1e6cc97 100644 --- a/smartdc/datacenter.py +++ b/smartdc/datacenter.py @@ -250,7 +250,7 @@ def request(self, method, path, headers=None, data=None, **kwargs): print(resp.content, file=sys.stderr) resp.raise_for_status() if resp.content: - ctype = re.match('application/json(; +charset *= *([a-zA-z0-9-_]+))? *', + ctype = re.match('application/json(; +charset *= *([a-zA-z0-9-_]+))?', resp.headers['content-type']) if ctype: return (json.loads(resp.content, ctype.groups('utf-8')[1]), resp) From ad26d6ed6ab12ff7c22cf226a10ab2b68ca2d2c1 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Tue, 25 Jun 2013 21:44:05 +0200 Subject: [PATCH 08/10] Add basic inbound rules creation and reading; minor fixes --- smartdc/datacenter.py | 4 +- smartdc/network.py | 91 +++++++++++++++++++++++++++++++++++++++++++ smartdc/tef.py | 3 +- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/smartdc/datacenter.py b/smartdc/datacenter.py index 1e6cc97..47eafca 100644 --- a/smartdc/datacenter.py +++ b/smartdc/datacenter.py @@ -236,7 +236,7 @@ def request(self, method, path, headers=None, data=None, **kwargs): if self.verbose: print("%s\t%s\t%s" % (datetime.now().isoformat(), method, full_path), - file=self.verbose) + file=sys.stderr) resp = requests.request(method, full_path, auth=self.auth, headers=request_headers, data=jdata, verify=self.verify, **kwargs) @@ -272,7 +272,7 @@ def api(self): if self.verbose: print("%s\t%s\t%s" % (datetime.now().isoformat(), 'GET', self.base_url), - file=self.verbose) + file=sys.stderr) resp = requests.request('GET', self.base_url, verify=self.verify) if 400 <= resp.status_code < 499: resp.raise_for_status() diff --git a/smartdc/network.py b/smartdc/network.py index c702349..33f049a 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -2,6 +2,7 @@ import time import uuid import json +import re __all__ = ['Network'] @@ -245,3 +246,93 @@ def get_outbound(self): j, _ = self.datacenter.request('GET', self.path + '/outbound') return j['enabled'] + def get_inbound_rules(self): + """ + :: + + GET /:login/networks/:id/inbound + + :Returns: a list containing all the inbound port forwarding + rules for the current Netowrk. + :rtype: :py:class:`list` of :py:class:`dict` + """ + j, _ = self.datacenter.request('GET', self.path + '/inbound') + return j + + def add_inbound_rule(self, name, start_port, destination_ip, end_port=None, + protocols=None, source_subnet=None, destination_base_port=None): + """ + :: + + PUT /:login/networks/:id/inbound + + :param name: the name of the new inbound port forwarding rule + to add. Up to 32 letters, digits and hyphens. Required. + :type name: :py:class:`basestring` + + :param start_port: first inbound port to forward according to this + rule (0...65535). Required. + :type start_port: :py:class:`int` + + :param end_port: last inbound port to forward according to this rule + (0...65535). If ommitted, the `start_port` will be taken. + :type end_port: :py:class:`int` + + :param protocols: list of protocols to forward according to this list. + Can be a list of strings or a single value. If omitted, both TCP + and UPD will be forwarded. + :type protocols: :py:class:`list` or :py:class:`basestring` + + :param source_subnet: a CIDR specifying the origin of the traffic to + be forwarded. If omitted, it will be set to '0.0.0.0/0', thus + forwarding all incoming traffic. + :type source_subnet: :py:class:`basestring` + + :param destination_ip: internal IP to which the inbound traffic + accepted by this rule should be forwarded to. Required. + :type destination_ip: :py:class:`basestring` + + :param destination_base_port: (first) port of the destination IP to + forward the inbound traffic to (0...65535). If omitted, the + `start_port` will be taken. + :type destination_base_port: :py:class:`int` + + :Returns: a dict containing the new inbound port forwarding rule. + :rtype: :py:class:`dict` + """ + params = {} + assert re.match(r'[a-zA-Z0-9-_]{1,32}', name), "Illegal name" + params['name'] = name + assert start_port>=0 and start_port<=65535, "Illegal start_port" + params['start_port'] = start_port + if end_port: + assert end_port>=start_port and end_port<=65535, \ + "Illegal end_port" + else: + end_port = start_port + params['end_port'] = end_port + if protocols: + if isinstance(protocols, basestring): + protocols = [protocols] + else: + protocols = ["tcp", "udp"] + params['protocols'] = protocols + if source_subnet: + assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(\/[0-9]+)?', \ + source_subnet), "Illegal source_subnet" + if not source_subnet.find('/'): + source_subnet = source_subnet + "/32" + params['source_subnet'] = source_subnet + assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', destination_ip), \ + "Illegal destination_ip" + params['destination_ip'] = destination_ip + if destination_base_port: + assert destination_base_port>=0 and destination_base_port<=65535, \ + "Illegal destination_base_port" + else: + destination_base_port = start_port + params['destination_base_port'] = destination_base_port + j, r = self.datacenter.request('POST', self.path + '/inbound', data=params) + r.raise_for_status() + return j + diff --git a/smartdc/tef.py b/smartdc/tef.py index 42684be..415054c 100644 --- a/smartdc/tef.py +++ b/smartdc/tef.py @@ -50,7 +50,8 @@ def create_network(self, name, subnet, resolver_ips=None): params['resolver_ips'] = resolver_ips j, r = self.request('POST', 'networks', data=params) if r.status_code >= 400: - print(j, file=sys.stderr) + if self.verbose: + print(j, file=sys.stderr) r.raise_for_status() return Network(datacenter=self, data=j) From 77bdbeeeea18c83a33f4434d91d34e91ddac13aa Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Wed, 26 Jun 2013 19:46:22 +0200 Subject: [PATCH 09/10] Move TELEFONICA_LOCATIONS to tef.py, add ACENS_LOCATIONS --- smartdc/__init__.py | 2 +- smartdc/datacenter.py | 10 +--------- smartdc/tef.py | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/smartdc/__init__.py b/smartdc/__init__.py index 27d7b55..8fe31b8 100644 --- a/smartdc/__init__.py +++ b/smartdc/__init__.py @@ -2,7 +2,7 @@ from .machine import * from .legacy import LegacyDataCenter from .network import * -from .tef import TefDataCenter +from .tef import * from ._version import get_versions __version__ = get_versions()['version'] diff --git a/smartdc/datacenter.py b/smartdc/datacenter.py index 47eafca..1ff4e5a 100644 --- a/smartdc/datacenter.py +++ b/smartdc/datacenter.py @@ -15,8 +15,7 @@ __version__ = get_versions()['version'] del get_versions -__all__ = ['DataCenter', 'KNOWN_LOCATIONS', - 'TELEFONICA_LOCATIONS', 'DEFAULT_LOCATION'] +__all__ = ['DataCenter', 'KNOWN_LOCATIONS', 'DEFAULT_LOCATION'] API_HOST_SUFFIX = '.api.joyentcloud.com' @@ -27,13 +26,6 @@ u'eu-ams-1': u'https://eu-ams-1.api.joyentcloud.com', } -TELEFONICA_LOCATIONS = { - u'London': u'https://api-eu-lon-1.instantservers.telefonica.com', - u'eu-lon-1': u'https://api-eu-lon-1.instantservers.telefonica.com', - u'Madrid': u'https://api-eu-mad-1.instantservers.telefonica.com', - u'eu-mad-1': u'https://api-eu-mad-1.instantservers.telefonica.com', -} - DEFAULT_LOCATION = 'us-west-1' DEFAULT_HEADERS = { diff --git a/smartdc/tef.py b/smartdc/tef.py index 415054c..b95f300 100644 --- a/smartdc/tef.py +++ b/smartdc/tef.py @@ -6,6 +6,22 @@ import re import sys +__all__ = ['TefDataCenter', 'TELEFONICA_LOCATIONS', 'ACENS_LOCATIONS'] + +TELEFONICA_LOCATIONS = { + u'London': u'https://api-eu-lon-1.instantservers.telefonica.com', + u'eu-lon-1': u'https://api-eu-lon-1.instantservers.telefonica.com', + u'Madrid': u'https://api-eu-mad-1.instantservers.telefonica.com', + u'eu-mad-1': u'https://api-eu-mad-1.instantservers.telefonica.com', +} + +ACENS_LOCATIONS = { + u'London': u'https://api-lon.instantservers.es', + u'lon': u'https://api-lon.instantservers.es', + u'Madrid': u'https://api-mad.instantservers.es', + u'mad': u'https://api-mad.instantservers.es', +} + class TefDataCenter(LegacyDataCenter): """ This class provides support for the network extensiones present @@ -51,8 +67,8 @@ def create_network(self, name, subnet, resolver_ips=None): j, r = self.request('POST', 'networks', data=params) if r.status_code >= 400: if self.verbose: - print(j, file=sys.stderr) - r.raise_for_status() + print(j, file=sys.stderr) + r.raise_for_status() return Network(datacenter=self, data=j) def raw_network_data(self, network_id): From d20b8f2aba42dbc98e56857aed74b37aa9492961 Mon Sep 17 00:00:00 2001 From: ajsanchez Date: Fri, 28 Jun 2013 23:14:19 +0200 Subject: [PATCH 10/10] Add get/set_inbound_rule_status, delete_inbound_rule, delete_all_inbound_rules; new r.raise_for_status() checks --- smartdc/network.py | 75 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/smartdc/network.py b/smartdc/network.py index 33f049a..c6d05b9 100644 --- a/smartdc/network.py +++ b/smartdc/network.py @@ -214,13 +214,13 @@ def poll_while(self, status, interval=2): while self.status() == status: time.sleep(interval) - def set_outbound(self, enabled): + def set_outbound_status(self, enabled): """ :: PUT /:login/networks/:id/outbound - :param enabled: new status for the curret network outbound PAT (Port + :param enabled: new status for the current network outbound PAT (Port Address Translation). :type enabled: :py:class:`bool` @@ -229,11 +229,12 @@ def set_outbound(self, enabled): :rtype: :py:class:`bool` """ assert isinstance(enabled, bool), "Illegal status" - j, _ = self.datacenter.request('PUT', self.path + '/outbound', + j, r = self.datacenter.request('PUT', self.path + '/outbound', data={ 'enabled': enabled }) + r.raise_for_status() return j['enabled'] - def get_outbound(self): + def get_outbound_status(self): """ :: @@ -243,7 +244,8 @@ def get_outbound(self): status. :rtype: :py:class:`bool` """ - j, _ = self.datacenter.request('GET', self.path + '/outbound') + j, r = self.datacenter.request('GET', self.path + '/outbound') + r.raise_for_status() return j['enabled'] def get_inbound_rules(self): @@ -335,4 +337,67 @@ def add_inbound_rule(self, name, start_port, destination_ip, end_port=None, j, r = self.datacenter.request('POST', self.path + '/inbound', data=params) r.raise_for_status() return j + + def set_inbound_rule_status(self, rule_id, enabled): + """ + :: + + PUT /:login/networks/:id/inbound/:rule_id + + :param rule_id: id of the inbound port forwarding rule. + :type rule_id: :py:class:`basestring` + :param enabled: new status for the former inbound port forwarding rule. + :type enabled: :py:class:`bool` + + :Returns: the updated inbound port forwarding rule status. + :rtype: :py:class:`bool` + """ + assert isinstance(rule_id, basestring), "Illegal rule_id" + assert isinstance(enabled, bool), "Illegal status" + j, r = self.datacenter.request('PUT', self.path + '/inbound/' + rule_id, + data={ 'enabled': enabled }) + r.raise_for_status() + return j['enabled'] + + def get_outbound_rule_status(self, rule_id): + """ + :: + + GET /:login/networks/:id/inbound/:rule_id + + :param rule_id: id of the inbound port forwarding rule. + :type rule_id: :py:class:`basestring` + + :Returns: the current status of the specified inbound port + forwarding rule. + :rtype: :py:class:`bool` + """ + assert isinstance(rule_id, basestring), "Illegal rule_id" + j, r = self.datacenter.request('GET', self.path + '/inbound/' + rule_id) + r.raise_for_status() + return j['enabled'] + + def delete_inbound_rule(self, rule_id): + """ + + DELETE /:login/networks/:id/inbound/:rule_id + + :param rule_id: id of the inbound port forwarding rule to delete. + :type rule_id: :py:class:`basestring` + """ + assert isinstance(rule_id, basestring), "Illegal rule_id" + _, r = self.datacenter.request('DELETE', self.path + '/inbound/' + rule_id) + r.raise_for_status() + + def delete_all_inbound_rules(self): + """ + + DELETE /:login/networks/:id/inbound/:rule_id + + Deletes all inbound port forwarding rules of this network. + """ + for rule in self.get_inbound_rules(): + _, r = self.datacenter.request('DELETE', self.path + '/inbound/' + \ + rule['id']) + r.raise_for_status()