Skip to content

Commit f91507a

Browse files
authored
Merge pull request #937 from DanielArndt/fix-missing-controller-yaml
#937 #### Description When Juju is not bootstrapped, `controllers.yaml` does not yet exist. This throws a `FileNotFoundError` which is hard to decipher the root cause. This is a bit of a contract change, but it appears the only place this is used (at least internally) already guards against `None`. I added a hint to the error message in this case to indicate that Juju may not be bootstrapped. See Notes & Discussion for alternatives. #### QA Steps - Try calling `jujudata.current_controller()` on a machine that is not yet bootstrapped. #### Notes & Discussion An alternative to this might be raising a `JujuError` directly from `FileJujuData.current_controller()`. This should also probably be applied to `FileJujuData.controllers()`, `FileJujuData.models()`, `FileJujuData.accounts()`, and `FileJujuData.credentials()` calls. Happy to make changes here if there's a preferred approach, but I thought I'd open the discussion.
2 parents 6aa065e + 7a0ffb7 commit f91507a

4 files changed

Lines changed: 97 additions & 61 deletions

File tree

juju/client/connector.py

Lines changed: 66 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,38 @@
55
import logging
66

77
import macaroonbakery.httpbakery as httpbakery
8+
9+
from juju.client import client
810
from juju.client.connection import Connection
911
from juju.client.gocookies import GoCookieJar, go_to_py_cookie
10-
from juju.client.jujudata import FileJujuData, API_ENDPOINTS_KEY
12+
from juju.client.jujudata import API_ENDPOINTS_KEY, FileJujuData
1113
from juju.client.proxy.factory import proxy_from_config
1214
from juju.errors import JujuConnectionError, JujuError
13-
from juju.client import client
1415
from juju.version import SUPPORTED_MAJOR_VERSION, TARGET_JUJU_VERSION
1516

16-
log = logging.getLogger('connector')
17+
log = logging.getLogger("connector")
1718

1819

1920
class NoConnectionException(Exception):
20-
'''Raised by Connector when the connection method is called
21-
and there is no current connection.'''
21+
"""Raised by Connector when the connection method is called
22+
and there is no current connection."""
23+
2224
pass
2325

2426

2527
class Connector:
26-
'''This class abstracts out a reconnectable client that can connect
28+
"""This class abstracts out a reconnectable client that can connect
2729
to controllers and models found in the Juju data files.
28-
'''
30+
"""
31+
2932
def __init__(
3033
self,
3134
max_frame_size=None,
3235
bakery_client=None,
3336
jujudata=None,
3437
):
35-
'''Initialize a connector that will use the given parameters
36-
by default when making a new connection'''
38+
"""Initialize a connector that will use the given parameters
39+
by default when making a new connection"""
3740
self.max_frame_size = max_frame_size
3841
self.bakery_client = bakery_client
3942
self._connection = None
@@ -43,32 +46,32 @@ def __init__(
4346
self.jujudata = jujudata or FileJujuData()
4447

4548
def is_connected(self):
46-
'''Report whether there is a currently connected controller or not'''
49+
"""Report whether there is a currently connected controller or not"""
4750
return self._connection is not None
4851

4952
def connection(self):
50-
'''Return the current connection; raises an exception if there
51-
is no current connection.'''
53+
"""Return the current connection; raises an exception if there
54+
is no current connection."""
5255
if not self.is_connected():
53-
raise NoConnectionException('not connected')
56+
raise NoConnectionException("not connected")
5457
return self._connection
5558

5659
async def connect(self, **kwargs):
5760
"""Connect to an arbitrary Juju model.
5861
5962
kwargs are passed through to Connection.connect()
6063
"""
61-
kwargs.setdefault('max_frame_size', self.max_frame_size)
62-
kwargs.setdefault('bakery_client', self.bakery_client)
63-
if 'macaroons' in kwargs:
64-
if not kwargs['bakery_client']:
65-
kwargs['bakery_client'] = httpbakery.Client()
66-
if not kwargs['bakery_client'].cookies:
67-
kwargs['bakery_client'].cookies = GoCookieJar()
68-
jar = kwargs['bakery_client'].cookies
69-
for macaroon in kwargs.pop('macaroons'):
64+
kwargs.setdefault("max_frame_size", self.max_frame_size)
65+
kwargs.setdefault("bakery_client", self.bakery_client)
66+
if "macaroons" in kwargs:
67+
if not kwargs["bakery_client"]:
68+
kwargs["bakery_client"] = httpbakery.Client()
69+
if not kwargs["bakery_client"].cookies:
70+
kwargs["bakery_client"].cookies = GoCookieJar()
71+
jar = kwargs["bakery_client"].cookies
72+
for macaroon in kwargs.pop("macaroons"):
7073
jar.set_cookie(go_to_py_cookie(macaroon))
71-
if 'debug_log_conn' in kwargs:
74+
if "debug_log_conn" in kwargs:
7275
assert self._connection
7376
self._log_connection = await Connection.connect(**kwargs)
7477
else:
@@ -83,21 +86,28 @@ async def connect(self, **kwargs):
8386
self._connection = await Connection.connect(**kwargs)
8487

8588
# Check if we support the target controller
86-
juju_server_version = self._connection.info['server-version']
89+
juju_server_version = self._connection.info["server-version"]
8790
if not juju_server_version.startswith(TARGET_JUJU_VERSION):
88-
log.debug("This version was tested using {} juju version {} may have compatibility issues".format(TARGET_JUJU_VERSION, juju_server_version))
89-
if not self._connection.info['server-version'].startswith(SUPPORTED_MAJOR_VERSION):
90-
raise JujuConnectionError("juju server-version %s not supported" % juju_server_version)
91+
log.debug(
92+
"This version was tested using {} juju version {} may have compatibility issues".format(
93+
TARGET_JUJU_VERSION, juju_server_version
94+
)
95+
)
96+
if not self._connection.info["server-version"].startswith(
97+
SUPPORTED_MAJOR_VERSION
98+
):
99+
raise JujuConnectionError(
100+
"juju server-version %s not supported" % juju_server_version
101+
)
91102

92103
async def disconnect(self, entity):
93-
"""Shut down the watcher task and close websockets.
94-
"""
104+
"""Shut down the watcher task and close websockets."""
95105
if self._connection:
96-
log.debug(f'Connector: closing {entity} connection')
106+
log.debug(f"Connector: closing {entity} connection")
97107
await self._connection.close()
98108
self._connection = None
99109
if self._log_connection:
100-
log.debug('Also closing debug-log connection')
110+
log.debug("Also closing debug-log connection")
101111
await self._log_connection.close()
102112
self._log_connection = None
103113

@@ -107,21 +117,19 @@ async def connect_controller(self, controller_name=None, specified_facades=None)
107117
"""
108118
if not controller_name:
109119
controller_name = self.jujudata.current_controller()
110-
if not controller_name:
111-
raise JujuConnectionError('No current controller')
112120

113121
controller = self.jujudata.controllers()[controller_name]
114122
endpoints = controller[API_ENDPOINTS_KEY]
115123
accounts = self.jujudata.accounts().get(controller_name, {})
116124

117-
proxy = proxy_from_config(controller.get('proxy-config', None))
125+
proxy = proxy_from_config(controller.get("proxy-config", None))
118126

119127
await self.connect(
120128
endpoint=endpoints,
121129
uuid=None,
122-
username=accounts.get('user'),
123-
password=accounts.get('password'),
124-
cacert=controller.get('ca-cert'),
130+
username=accounts.get("user"),
131+
password=accounts.get("password"),
132+
cacert=controller.get("ca-cert"),
125133
bakery_client=self.bakery_client_for_controller(controller_name),
126134
specified_facades=specified_facades,
127135
proxy=proxy,
@@ -142,57 +150,57 @@ async def connect_model(self, _model_name=None, **kwargs):
142150
except JujuError as e:
143151
raise JujuConnectionError(e.message) from e
144152
if controller is None:
145-
raise JujuConnectionError('Controller {} not found'.format(
146-
controller_name))
153+
raise JujuConnectionError("Controller {} not found".format(controller_name))
147154
endpoints = controller[API_ENDPOINTS_KEY]
148155
account = self.jujudata.accounts().get(controller_name, {})
149-
models = self.jujudata.models().get(controller_name, {}).get('models',
150-
{})
156+
models = self.jujudata.models().get(controller_name, {}).get("models", {})
151157
model_uuid = None
152158
if _model_name in models:
153-
model_uuid = models[_model_name]['uuid']
159+
model_uuid = models[_model_name]["uuid"]
154160
else:
155161
# let's try to find it through the actual controller
156162
await self.connect_controller(controller_name=controller_name)
157163
# get the facade
158164
controller_facade = client.ControllerFacade.from_connection(
159-
self.connection())
165+
self.connection()
166+
)
160167
# get all the user models from the api
161168
response = await controller_facade.AllModels()
162169
# search the one that contains admin/model_name
163170
for user_model in response.user_models:
164-
if 'admin/' + user_model.model.name == _model_name:
171+
if "admin/" + user_model.model.name == _model_name:
165172
model_uuid = user_model.model.uuid
166173

167174
if model_uuid is None:
168-
raise JujuConnectionError('Model not found: {}'.format(_model_name))
175+
raise JujuConnectionError("Model not found: {}".format(_model_name))
169176

170-
proxy = proxy_from_config(controller.get('proxy-config', None))
177+
proxy = proxy_from_config(controller.get("proxy-config", None))
171178

172179
# TODO remove the need for base.CleanModel to subclass
173180
# JujuData.
174-
kwargs.update(endpoint=endpoints,
175-
uuid=model_uuid,
176-
username=account.get('user'),
177-
password=account.get('password'),
178-
cacert=controller.get('ca-cert'),
179-
bakery_client=self.bakery_client_for_controller(controller_name),
180-
proxy=proxy)
181+
kwargs.update(
182+
endpoint=endpoints,
183+
uuid=model_uuid,
184+
username=account.get("user"),
185+
password=account.get("password"),
186+
cacert=controller.get("ca-cert"),
187+
bakery_client=self.bakery_client_for_controller(controller_name),
188+
proxy=proxy,
189+
)
181190
await self.connect(**kwargs)
182191
# TODO this might be a good spot to trigger refreshing the
183192
# local cache (the connection to the model might help)
184-
self.model_name = controller_name + ':' + _model_name
193+
self.model_name = controller_name + ":" + _model_name
185194
return model_uuid
186195

187196
def bakery_client_for_controller(self, controller_name):
188-
'''Make a copy of the bakery client with a the appropriate controller's
197+
"""Make a copy of the bakery client with a the appropriate controller's
189198
cookiejar in it.
190-
'''
199+
"""
191200
bakery_client = self.bakery_client
192201
if bakery_client:
193202
bakery_client = copy.copy(bakery_client)
194203
else:
195204
bakery_client = httpbakery.Client()
196-
bakery_client.cookies = self.jujudata.cookies_for_controller(
197-
controller_name)
205+
bakery_client.cookies = self.jujudata.cookies_for_controller(controller_name)
198206
return bakery_client

juju/client/jujudata.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import os
77
import pathlib
88

9-
from juju.client import client as jujuclient
109
import yaml
10+
1111
from juju import tag
12+
from juju.client import client as jujuclient
1213
from juju.client.gocookies import GoCookieJar
13-
from juju.errors import JujuError, PylibjujuProgrammingError
14+
from juju.errors import (JujuControllerNotFoundError, JujuError,
15+
PylibjujuProgrammingError)
1416
from juju.utils import juju_config_dir
1517

1618
API_ENDPOINTS_KEY = 'api-endpoints'
@@ -77,7 +79,10 @@ def refresh(self):
7779

7880
def current_controller(self):
7981
'''Return the current controller name'''
80-
return self._load_yaml('controllers.yaml', 'current-controller')
82+
try:
83+
return self._load_yaml('controllers.yaml', 'current-controller')
84+
except FileNotFoundError:
85+
raise JujuControllerNotFoundError('No controllers.yaml file found. python-libjuju requires a bootstrapped Juju controller.')
8186

8287
def current_model(self, controller_name=None, model_only=False):
8388
'''Return the current model, qualified by its controller name.

juju/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ class JujuModelConfigError(JujuConfigError):
123123
pass
124124

125125

126+
class JujuControllerNotFoundError(JujuError):
127+
pass
128+
129+
126130
class AbstractMethodError(Exception):
127131
pass
128132

tests/unit/test_jujudata.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2023 Canonical Ltd.
2+
# Licensed under the Apache V2, see LICENCE file for details.
3+
4+
import unittest
5+
6+
import mock
7+
import pytest
8+
9+
from juju.client.jujudata import FileJujuData
10+
from juju.errors import JujuControllerNotFoundError
11+
12+
13+
class TestJujuData(unittest.IsolatedAsyncioTestCase):
14+
@mock.patch("io.open")
15+
async def test_verify_controller_uninitialized(self, mock_io_open):
16+
mock_io_open.side_effect = FileNotFoundError()
17+
jujudata = FileJujuData()
18+
with pytest.raises(JujuControllerNotFoundError):
19+
jujudata.current_controller()

0 commit comments

Comments
 (0)