diff -Nru python-jujuclient-0.50.1/debian/changelog python-jujuclient-0.50.3/debian/changelog --- python-jujuclient-0.50.1/debian/changelog 2015-03-02 09:37:24.000000000 +0000 +++ python-jujuclient-0.50.3/debian/changelog 2015-11-05 09:54:56.000000000 +0000 @@ -1,3 +1,21 @@ +python-jujuclient (0.50.3-1~ubuntu14.04.1~ppa1) trusty; urgency=medium + + * No-change backport to trusty + + -- Francesco Banconi Thu, 05 Nov 2015 10:54:56 +0100 + +python-jujuclient (0.50.3-1) wily; urgency=medium + + * New build for release 0.50.3. + + -- Francesco Banconi Thu, 05 Nov 2015 10:49:19 +0100 + +python-jujuclient (0.50.2-1) wily; urgency=medium + + * New build for release 0.50.2. + + -- Francesco Banconi Tue, 27 Oct 2015 14:59:04 +0100 + python-jujuclient (0.50.1-2) utopic; urgency=low * New build to work around a PPA race. diff -Nru python-jujuclient-0.50.1/jujuclient.egg-info/PKG-INFO python-jujuclient-0.50.3/jujuclient.egg-info/PKG-INFO --- python-jujuclient-0.50.1/jujuclient.egg-info/PKG-INFO 2015-02-27 02:49:06.000000000 +0000 +++ python-jujuclient-0.50.3/jujuclient.egg-info/PKG-INFO 2015-11-04 22:50:23.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: jujuclient -Version: 0.50.1 +Version: 0.50.3 Summary: A juju-core/gojuju simple synchronous python api client. Home-page: http://juju.ubuntu.com Author: Kapil Thangavelu diff -Nru python-jujuclient-0.50.1/jujuclient.egg-info/requires.txt python-jujuclient-0.50.3/jujuclient.egg-info/requires.txt --- python-jujuclient-0.50.1/jujuclient.egg-info/requires.txt 2015-02-27 02:49:06.000000000 +0000 +++ python-jujuclient-0.50.3/jujuclient.egg-info/requires.txt 2015-11-04 22:50:23.000000000 +0000 @@ -1 +1,2 @@ +PyYAML websocket-client>=0.18.0 \ No newline at end of file diff -Nru python-jujuclient-0.50.1/jujuclient.py python-jujuclient-0.50.3/jujuclient.py --- python-jujuclient-0.50.1/jujuclient.py 2015-02-27 02:47:45.000000000 +0000 +++ python-jujuclient-0.50.3/jujuclient.py 2015-11-04 16:16:38.000000000 +0000 @@ -19,6 +19,7 @@ import logging import os import pprint +import re import shutil import signal import socket @@ -53,7 +54,6 @@ websocket.logger = logging.getLogger("websocket") - log = logging.getLogger("jujuclient") @@ -183,7 +183,7 @@ try: return Connector.connect_socket(endpoint, cert_path) except socket.error as err: - if not err.errno in self.retry_conn_errors: + if err.errno not in self.retry_conn_errors: raise time.sleep(1) continue @@ -207,20 +207,61 @@ return s def parse_env(self, env_name): - import yaml jhome = os.path.expanduser( os.environ.get('JUJU_HOME', '~/.juju')) + + # Look in the cache file first. + cache_file = os.path.join(jhome, 'environments', 'cache.yaml') jenv = os.path.join(jhome, 'environments', '%s.jenv' % env_name) + + if os.path.exists(cache_file): + try: + return jhome, self.environment_from_cache(env_name, cache_file) + except EnvironmentNotBootstrapped: + pass + # Fall through to getting the info from the jenv if not os.path.exists(jenv): raise EnvironmentNotBootstrapped(env_name) + return jhome, self.environment_from_jenv(jenv) + def environment_from_cache(self, env_name, cache_filename): + import yaml + with open(cache_filename) as fh: + data = yaml.safe_load(fh.read()) + try: + # environment holds: + # user, env-uuid, server-uuid + environment = data['environment'][env_name] + server = data['server-data'][environment['server-uuid']] + return { + 'user': environment['user'], + 'password': server['identities'][environment['user']], + 'environ-uuid': environment['env-uuid'], + 'server-uuid': environment['server-uuid'], + 'state-servers': server['api-endpoints'], + 'ca-cert': server['ca-cert'], + } + except KeyError: + raise EnvironmentNotBootstrapped(env_name) + + def environment_from_jenv(self, jenv): + import yaml with open(jenv) as fh: data = yaml.safe_load(fh.read()) - return jhome, data + return data + + @staticmethod + def split_host_port(server): + m = re.match('(.*):(.*)', server) + if not m: + raise ValueError("Not an ipaddr/port {!r}".format(server)) + address = m.group(1).strip("[]") + port = m.group(2) + return address, port def is_server_available(self, server): """ Given address/port, return true/false if it's up """ - address, port = server.split(":") + address, port = Connector.split_host_port(server) try: socket.create_connection((address, port), 3) return True @@ -253,20 +294,45 @@ class RPC(object): - + _upgrade_retry_delay_secs = 1 + _upgrade_retry_count = 60 _auth = False _request_id = 0 _debug = False _reconnect_params = None + conn = None def _rpc(self, op): if not self._auth and not op.get("Request") == "Login": raise LoginRequired() - if not 'Params' in op: + if 'Params' not in op: op['Params'] = {} op['RequestId'] = self._request_id self._request_id += 1 + result = self._rpc_retry_if_upgrading(op) + if 'Error' in result: + # The backend disconnects us on err, bug: http://pad.lv/1160971 + self.conn.connected = False + raise EnvError(result) + return result['Response'] + + def _rpc_retry_if_upgrading(self, op): + """If Juju is upgrading when the specified rpc call is made, + retry the call.""" + retry_count = 0 + result = {'Response': ''} + while retry_count <= self._upgrade_retry_count: + result = self._send_request(op) + if 'Error' in result and 'upgrade in progress' in result['Error']: + log.info("Juju upgrade in progress...") + retry_count += 1 + time.sleep(self._upgrade_retry_delay_secs) + continue + break + return result + + def _send_request(self, op): if self._debug: log.debug("rpc request:\n%s" % (json.dumps(op, indent=2))) self.conn.send(json.dumps(op)) @@ -274,12 +340,7 @@ result = json.loads(raw) if self._debug: log.debug("rpc response:\n%s" % (json.dumps(result, indent=2))) - - if 'Error' in result: - # The backend disconnects us on err, bug: http://pad.lv/1160971 - self.conn.connected = False - raise EnvError(result) - return result['Response'] + return result def login(self, password, user="user-admin"): """Login gets shared to watchers for reconnect.""" @@ -394,13 +455,23 @@ class TimeoutWatcher(Watcher): # A simple non concurrent watch using signals.. - _timeout = None + def __init__(self, *args, **kw): + super(TimeoutWatcher, self).__init__(*args, **kw) + self.start_time = time.time() + self._timeout = 0 + + def time_remaining(self): + """Return number of seconds until this watch times out. + + """ + return int(self._timeout - (time.time() - self.start_time)) def set_timeout(self, timeout): + self.start_time = time.time() self._timeout = timeout def next(self): - with self._set_alarm(self._timeout): + with self._set_alarm(self.time_remaining()): return super(TimeoutWatcher, self).next() # py3 compat @@ -409,6 +480,9 @@ @classmethod @contextmanager def _set_alarm(cls, timeout): + if timeout < 0: + raise TimeoutError() + try: handler = signal.getsignal(signal.SIGALRM) if callable(handler): @@ -478,7 +552,9 @@ def _http_conn(self): endpoint = self.endpoint.replace('wss://', '') host, remainder = endpoint.split(':', 1) - port, _ = remainder.split('/', 1) + port = remainder + if remainder.endswith('/'): + port, _ = remainder.split('/', 1) conn = HTTPSConnection(host, port) headers = { 'Authorization': 'Basic %s' % b64encode( @@ -803,14 +879,40 @@ "Params": {"Commands": command, "Timeout": timeout}}) - def run(self, targets, command, timeout=None): + def run(self, command, timeout=None, machines=None, + services=None, units=None): """Run a shell command on the targets (services, units, or machines). + At least one target must be specified machines || services || units """ - return self._rpc({ + + assert not (not machines and not services and not units), \ + "You must specify a target" + + rpc_dict = { "Type": "Client", "Request": "Run", - "Params": {"Commands": command, - "Timeout": timeout}}) + "Params": { + "Commands": command, + "Timeout": timeout, + } + } + + if machines: + if not isinstance(machines, (list, tuple)): + machines = [machines] + rpc_dict["Params"].update({'Machines': machines}) + + if services: + if not isinstance(services, (list, tuple)): + services = [services] + rpc_dict["Params"].update({'Services': services}) + + if units: + if not isinstance(units, (list, tuple)): + units = [units] + rpc_dict["Params"].update({'Units': units}) + + return self._rpc(rpc_dict) # Machine ops def add_machine(self, series="", constraints=None, @@ -972,6 +1074,8 @@ if connection is None: watch_env = Environment(self.endpoint) watch_env.login(**self._creds) + if self._debug: + watch_env._debug = True else: watch_env = connection @@ -1352,14 +1456,14 @@ def _format_user_tag(self, n): if not n.startswith('user-'): n = "user-%s" % n - if not '@' in n: + if '@' not in n: n = "%s@local" % n return n def rpc(self, op): - if not 'Type' in op: + if 'Type' not in op: op['Type'] = self.name - if not 'Version' in op: + if 'Version' not in op: op['Version'] = self.version return self.env._rpc(op) @@ -1439,7 +1543,7 @@ users = [] for e in entities: - if not 'username' in e or not 'password' in e: + if 'username' not in e or 'password' not in e: raise ValueError( "Invalid parameter for set password %s" % entities) users.append( @@ -1577,7 +1681,7 @@ def add(self, user, keys): return self.rpc({ - "Request": "ListKeys", + "Request": "AddKeys", "Params": {"User": user, "Keys": keys}}) @@ -1765,7 +1869,7 @@ return self.rpc({ 'Request': 'EnsureAvailability', 'Params': { - 'EnvironTag': "environment-%s" % self._env_uuid, + 'EnvironTag': "environment-%s" % self.env._env_uuid, 'NumStateServers': int(num_state_servers), 'Series': series, 'Constraints': self._format_constraints(constraints), @@ -2045,7 +2149,7 @@ d.pop("Series") d.pop("CharmURL") name = d.pop('Name') - ports = d.pop('Ports') + ports = d.pop('Ports') or [] tports = d.setdefault('Ports', []) # Workaround for lp:1425435 if ports: @@ -2070,7 +2174,7 @@ for ep in d['Endpoints']: svc_rels = self.data.setdefault( 'services', {}).setdefault( - ep['ServiceName'], {}).setdefault('relations', {}) + ep['ServiceName'], {}).setdefault('relations', {}) svc_rels.setdefault( ep['Relation']['Name'], []).append(ep['RemoteService']) diff -Nru python-jujuclient-0.50.1/PKG-INFO python-jujuclient-0.50.3/PKG-INFO --- python-jujuclient-0.50.1/PKG-INFO 2015-02-27 02:49:10.000000000 +0000 +++ python-jujuclient-0.50.3/PKG-INFO 2015-11-04 22:50:23.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: jujuclient -Version: 0.50.1 +Version: 0.50.3 Summary: A juju-core/gojuju simple synchronous python api client. Home-page: http://juju.ubuntu.com Author: Kapil Thangavelu diff -Nru python-jujuclient-0.50.1/setup.py python-jujuclient-0.50.3/setup.py --- python-jujuclient-0.50.1/setup.py 2015-02-27 02:47:58.000000000 +0000 +++ python-jujuclient-0.50.3/setup.py 2015-11-04 22:47:33.000000000 +0000 @@ -6,12 +6,12 @@ setup( name="jujuclient", - version="0.50.1", + version="0.50.3", description="A juju-core/gojuju simple synchronous python api client.", author="Kapil Thangavelu", author_email="kapil.foss@gmail.com", url="http://juju.ubuntu.com", - install_requires=["websocket-client>=0.18.0"], + install_requires=["PyYAML", "websocket-client>=0.18.0"], classifiers=[ "Development Status :: 4 - Beta", "Programming Language :: Python",