bug 563: Return initial-config-primitive uuids
[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.gocookies import go_to_py_cookie, GoCookieJar
8 from juju.client.jujudata import FileJujuData
9 from juju.errors import JujuConnectionError, JujuError
10
11 log = logging.getLogger('connector')
12
13
14 class NoConnectionException(Exception):
15 '''Raised by Connector when the connection method is called
16 and there is no current connection.'''
17 pass
18
19
20 class Connector:
21 '''This class abstracts out a reconnectable client that can connect
22 to controllers and models found in the Juju data files.
23 '''
24 def __init__(
25 self,
26 loop=None,
27 max_frame_size=None,
28 bakery_client=None,
29 jujudata=None,
30 ):
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()
40
41 def is_connected(self):
42 '''Report whether there is a currently connected controller or not'''
43 return self._connection is not None
44
45 def connection(self):
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
51
52 async def connect(self, **kwargs):
53 """Connect to an arbitrary Juju model.
54
55 kwargs are passed through to Connection.connect()
56 """
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)
69
70 async def disconnect(self):
71 """Shut down the watcher task and close websockets.
72 """
73 if self._connection:
74 log.debug('Closing model connection')
75 await self._connection.close()
76 self._connection = None
77
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.
81 """
82 if not controller_name:
83 controller_name = self.jujudata.current_controller()
84 if not controller_name:
85 raise JujuConnectionError('No current controller')
86
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, {})
92
93 await self.connect(
94 endpoint=endpoint,
95 uuid=None,
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),
100 )
101 self.controller_name = controller_name
102
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
106 will be used.
107
108 :param str model: <controller>:<model>
109 """
110
111 try:
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(
118 controller_name))
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',
124 {})
125 if model_name not in models:
126 raise JujuConnectionError('Model not found: {}'.format(model_name))
127
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
133 # subclass JujuData.
134 await self.connect(
135 endpoint=endpoint,
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),
141 )
142 self.controller_name = controller_name
143 self.model_name = controller_name + ':' + model_name
144
145 def bakery_client_for_controller(self, controller_name):
146 '''Make a copy of the bakery client with a the appropriate controller's
147 cookiejar in it.
148 '''
149 bakery_client = self.bakery_client
150 if bakery_client:
151 bakery_client = copy.copy(bakery_client)
152 else:
153 bakery_client = httpbakery.Client()
154 bakery_client.cookies = self.jujudata.cookies_for_controller(
155 controller_name)
156 return bakery_client