5 import macaroonbakery
.httpbakery
as httpbakery
6 from juju
.client
.connection
import Connection
7 from juju
.client
.gocookies
import go_to_py_cookie
, GoCookieJar
8 from juju
.client
.jujudata
import FileJujuData
9 from juju
.errors
import JujuConnectionError
, JujuError
11 log
= logging
.getLogger('connector')
14 class NoConnectionException(Exception):
15 '''Raised by Connector when the connection method is called
16 and there is no current connection.'''
21 '''This class abstracts out a reconnectable client that can connect
22 to controllers and models found in the Juju data files.
31 '''Initialize a connector that will use the given parameters
32 by default when making a new connection'''
33 self
.max_frame_size
= max_frame_size
34 self
.loop
= loop
or asyncio
.get_event_loop()
35 self
.bakery_client
= bakery_client
36 self
._connection
= None
37 self
.controller_name
= None
38 self
.model_name
= None
39 self
.jujudata
= jujudata
or FileJujuData()
41 def is_connected(self
):
42 '''Report whether there is a currently connected controller or not'''
43 return self
._connection
is not None
46 '''Return the current connection; raises an exception if there
47 is no current connection.'''
48 if not self
.is_connected():
49 raise NoConnectionException('not connected')
50 return self
._connection
52 async def connect(self
, **kwargs
):
53 """Connect to an arbitrary Juju model.
55 kwargs are passed through to Connection.connect()
57 kwargs
.setdefault('loop', self
.loop
)
58 kwargs
.setdefault('max_frame_size', self
.max_frame_size
)
59 kwargs
.setdefault('bakery_client', self
.bakery_client
)
60 if 'macaroons' in kwargs
:
61 if not kwargs
['bakery_client']:
62 kwargs
['bakery_client'] = httpbakery
.Client()
63 if not kwargs
['bakery_client'].cookies
:
64 kwargs
['bakery_client'].cookies
= GoCookieJar()
65 jar
= kwargs
['bakery_client'].cookies
66 for macaroon
in kwargs
.pop('macaroons'):
67 jar
.set_cookie(go_to_py_cookie(macaroon
))
68 self
._connection
= await Connection
.connect(**kwargs
)
70 async def disconnect(self
):
71 """Shut down the watcher task and close websockets.
74 log
.debug('Closing model connection')
75 await self
._connection
.close()
76 self
._connection
= None
78 async def connect_controller(self
, controller_name
=None):
79 """Connect to a controller by name. If the name is empty, it
80 connect to the current controller.
82 if not controller_name
:
83 controller_name
= self
.jujudata
.current_controller()
84 if not controller_name
:
85 raise JujuConnectionError('No current controller')
87 controller
= self
.jujudata
.controllers()[controller_name
]
88 # TODO change Connection so we can pass all the endpoints
89 # instead of just the first.
90 endpoint
= controller
['api-endpoints'][0]
91 accounts
= self
.jujudata
.accounts().get(controller_name
, {})
96 username
=accounts
.get('user'),
97 password
=accounts
.get('password'),
98 cacert
=controller
.get('ca-cert'),
99 bakery_client
=self
.bakery_client_for_controller(controller_name
),
101 self
.controller_name
= controller_name
103 async def connect_model(self
, model_name
=None):
104 """Connect to a model by name. If either controller or model
105 parts of the name are empty, the current controller and/or model
108 :param str model: <controller>:<model>
112 controller_name
, model_name
= self
.jujudata
.parse_model(model_name
)
113 controller
= self
.jujudata
.controllers().get(controller_name
)
114 except JujuError
as e
:
115 raise JujuConnectionError(e
.message
) from e
116 if controller
is None:
117 raise JujuConnectionError('Controller {} not found'.format(
119 # TODO change Connection so we can pass all the endpoints
120 # instead of just the first one.
121 endpoint
= controller
['api-endpoints'][0]
122 account
= self
.jujudata
.accounts().get(controller_name
, {})
123 models
= self
.jujudata
.models().get(controller_name
, {}).get('models',
125 if model_name
not in models
:
126 raise JujuConnectionError('Model not found: {}'.format(model_name
))
128 # TODO if there's no record for the required model name, connect
129 # to the controller to find out the model's uuid, then connect
130 # to that. This will let connect_model work with models that
131 # haven't necessarily synced with the local juju data,
132 # and also remove the need for base.CleanModel to
136 uuid
=models
[model_name
]['uuid'],
137 username
=account
.get('user'),
138 password
=account
.get('password'),
139 cacert
=controller
.get('ca-cert'),
140 bakery_client
=self
.bakery_client_for_controller(controller_name
),
142 self
.controller_name
= controller_name
143 self
.model_name
= controller_name
+ ':' + model_name
145 def bakery_client_for_controller(self
, controller_name
):
146 '''Make a copy of the bakery client with a the appropriate controller's
149 bakery_client
= self
.bakery_client
151 bakery_client
= copy
.copy(bakery_client
)
153 bakery_client
= httpbakery
.Client()
154 bakery_client
.cookies
= self
.jujudata
.cookies_for_controller(