64fbe4438692b8959117d4ee6f675efa697234b7
[osm/N2VC.git] / modules / libjuju / juju / client / connector.py
1 import asyncio
2 import logging
3 import copy
4
5 import macaroonbakery.httpbakery as httpbakery
6 from juju.client.connection import Connection
7 from juju.client.jujudata import FileJujuData
8 from juju.errors import JujuConnectionError, JujuError
9
10 log = logging.getLogger('connector')
11
12
13 class NoConnectionException(Exception):
14 '''Raised by Connector when the connection method is called
15 and there is no current connection.'''
16 pass
17
18
19 class Connector:
20 '''This class abstracts out a reconnectable client that can connect
21 to controllers and models found in the Juju data files.
22 '''
23 def __init__(
24 self,
25 loop=None,
26 max_frame_size=None,
27 bakery_client=None,
28 jujudata=None,
29 ):
30 '''Initialize a connector that will use the given parameters
31 by default when making a new connection'''
32 self.max_frame_size = max_frame_size
33 self.loop = loop or asyncio.get_event_loop()
34 self.bakery_client = bakery_client
35 self._connection = None
36 self.controller_name = None
37 self.model_name = None
38 self.jujudata = jujudata or FileJujuData()
39
40 def is_connected(self):
41 '''Report whether there is a currently connected controller or not'''
42 return self._connection is not None
43
44 def connection(self):
45 '''Return the current connection; raises an exception if there
46 is no current connection.'''
47 if not self.is_connected():
48 raise NoConnectionException('not connected')
49 return self._connection
50
51 async def connect(self, **kwargs):
52 """Connect to an arbitrary Juju model.
53
54 kwargs are passed through to Connection.connect()
55 """
56 kwargs.setdefault('loop', self.loop)
57 kwargs.setdefault('max_frame_size', self.max_frame_size)
58 kwargs.setdefault('bakery_client', self.bakery_client)
59 self._connection = await Connection.connect(**kwargs)
60
61 async def disconnect(self):
62 """Shut down the watcher task and close websockets.
63 """
64 if self._connection:
65 log.debug('Closing model connection')
66 await self._connection.close()
67 self._connection = None
68
69 async def connect_controller(self, controller_name=None):
70 """Connect to a controller by name. If the name is empty, it
71 connect to the current controller.
72 """
73 if not controller_name:
74 controller_name = self.jujudata.current_controller()
75 if not controller_name:
76 raise JujuConnectionError('No current controller')
77
78 controller = self.jujudata.controllers()[controller_name]
79 # TODO change Connection so we can pass all the endpoints
80 # instead of just the first.
81 endpoint = controller['api-endpoints'][0]
82 accounts = self.jujudata.accounts().get(controller_name, {})
83
84 await self.connect(
85 endpoint=endpoint,
86 uuid=None,
87 username=accounts.get('user'),
88 password=accounts.get('password'),
89 cacert=controller.get('ca-cert'),
90 bakery_client=self.bakery_client_for_controller(controller_name),
91 )
92 self.controller_name = controller_name
93
94 async def connect_model(self, model_name=None):
95 """Connect to a model by name. If either controller or model
96 parts of the name are empty, the current controller and/or model
97 will be used.
98
99 :param str model: <controller>:<model>
100 """
101
102 try:
103 controller_name, model_name = self.jujudata.parse_model(model_name)
104 controller = self.jujudata.controllers().get(controller_name)
105 except JujuError as e:
106 raise JujuConnectionError(e.message) from e
107 if controller is None:
108 raise JujuConnectionError('Controller {} not found'.format(
109 controller_name))
110 # TODO change Connection so we can pass all the endpoints
111 # instead of just the first one.
112 endpoint = controller['api-endpoints'][0]
113 account = self.jujudata.accounts().get(controller_name, {})
114 models = self.jujudata.models().get(controller_name, {}).get('models',
115 {})
116 if model_name not in models:
117 raise JujuConnectionError('Model not found: {}'.format(model_name))
118
119 # TODO if there's no record for the required model name, connect
120 # to the controller to find out the model's uuid, then connect
121 # to that. This will let connect_model work with models that
122 # haven't necessarily synced with the local juju data,
123 # and also remove the need for base.CleanModel to
124 # subclass JujuData.
125 await self.connect(
126 endpoint=endpoint,
127 uuid=models[model_name]['uuid'],
128 username=account.get('user'),
129 password=account.get('password'),
130 cacert=controller.get('ca-cert'),
131 bakery_client=self.bakery_client_for_controller(controller_name),
132 )
133 self.controller_name = controller_name
134 self.model_name = controller_name + ':' + model_name
135
136 def bakery_client_for_controller(self, controller_name):
137 '''Make a copy of the bakery client with a the appropriate controller's
138 cookiejar in it.
139 '''
140 bakery_client = self.bakery_client
141 if bakery_client:
142 bakery_client = copy.copy(bakery_client)
143 else:
144 bakery_client = httpbakery.Client()
145 bakery_client.cookies = self.jujudata.cookies_for_controller(
146 controller_name)
147 return bakery_client