Add support for deploying bundles
[osm/N2VC.git] / juju / client / connection.py
1 import io
2 import json
3 import logging
4 import os
5 import random
6 import shlex
7 import ssl
8 import string
9 import subprocess
10 import websockets
11
12 import yaml
13
14 from juju.errors import JujuAPIError
15
16 log = logging.getLogger("websocket")
17
18
19 class Connection:
20 """
21 Usage::
22
23 # Connect to an arbitrary api server
24 client = await Connection.connect(
25 api_endpoint, model_uuid, username, password, cacert)
26
27 # Connect using a controller/model name
28 client = await Connection.connect_model('local.local:default')
29
30 # Connect to the currently active model
31 client = await Connection.connect_current()
32
33 """
34 def __init__(self, endpoint, uuid, username, password, cacert=None):
35 self.endpoint = endpoint
36 self.uuid = uuid
37 self.username = username
38 self.password = password
39 self.cacert = cacert
40
41 self.__request_id__ = 0
42 self.addr = None
43 self.ws = None
44 self.facades = {}
45
46 @property
47 def is_open(self):
48 if self.ws:
49 return self.ws.open
50 return False
51
52 def _get_ssl(self, cert):
53 return ssl.create_default_context(
54 purpose=ssl.Purpose.CLIENT_AUTH, cadata=cert)
55
56 async def open(self, addr, cert=None):
57 kw = dict()
58 if cert:
59 kw['ssl'] = self._get_ssl(cert)
60 self.addr = addr
61 self.ws = await websockets.connect(addr, **kw)
62 return self
63
64 async def close(self):
65 await self.ws.close()
66
67 async def recv(self):
68 result = await self.ws.recv()
69 if result is not None:
70 result = json.loads(result)
71 return result
72
73 async def rpc(self, msg, encoder=None):
74 self.__request_id__ += 1
75 msg['request-id'] = self.__request_id__
76 if'params' not in msg:
77 msg['params'] = {}
78 if "version" not in msg:
79 msg['version'] = self.facades[msg['type']]
80 outgoing = json.dumps(msg, indent=2, cls=encoder)
81 await self.ws.send(outgoing)
82 result = await self.recv()
83 #log.debug("Send: %s", outgoing)
84 #log.debug("Recv: %s", result)
85 if result and 'error' in result:
86 raise JujuAPIError(result)
87 return result
88
89 async def clone(self):
90 """Return a new Connection, connected to the same websocket endpoint
91 as this one.
92
93 """
94 return await Connection.connect(
95 self.endpoint,
96 self.uuid,
97 self.username,
98 self.password,
99 self.cacert,
100 )
101
102 async def controller(self):
103 """Return a Connection to the controller at self.endpoint
104
105 """
106 return await Connection.connect(
107 self.endpoint,
108 None,
109 self.username,
110 self.password,
111 self.cacert,
112 )
113
114 @classmethod
115 async def connect(cls, endpoint, uuid, username, password, cacert=None):
116 """Connect to the websocket.
117
118 If uuid is None, the connection will be to the controller. Otherwise it
119 will be to the model.
120
121 """
122 if uuid:
123 url = "wss://{}/model/{}/api".format(endpoint, uuid)
124 else:
125 url = "wss://{}/api".format(endpoint)
126 client = cls(endpoint, uuid, username, password, cacert)
127 await client.open(url, cacert)
128 server_info = await client.login(username, password)
129 client.build_facades(server_info['facades'])
130 log.info("Driver connected to juju %s", url)
131
132 return client
133
134 @classmethod
135 async def connect_current(cls):
136 """Connect to the currently active model.
137
138 """
139 jujudata = JujuData()
140 controller_name = jujudata.current_controller()
141 controller = jujudata.controllers()[controller_name]
142 endpoint = controller['api-endpoints'][0]
143 cacert = controller.get('ca-cert')
144 accounts = jujudata.accounts()[controller_name]
145 username = accounts['user']
146 password = accounts['password']
147 models = jujudata.models()[controller_name]
148 model_name = models['current-model']
149 model_uuid = models['models'][model_name]['uuid']
150
151 return await cls.connect(
152 endpoint, model_uuid, username, password, cacert)
153
154 @classmethod
155 async def connect_model(cls, model):
156 """Connect to a model by name.
157
158 :param str model: <controller>:<model>
159
160 """
161 controller_name, model_name = model.split(':')
162
163 jujudata = JujuData()
164 controller = jujudata.controllers()[controller_name]
165 endpoint = controller['api-endpoints'][0]
166 cacert = controller.get('ca-cert')
167 accounts = jujudata.accounts()[controller_name]
168 username = accounts['user']
169 password = accounts['password']
170 models = jujudata.models()[controller_name]
171 model_uuid = models['models'][model_name]['uuid']
172
173 return await cls.connect(
174 endpoint, model_uuid, username, password, cacert)
175
176 def build_facades(self, info):
177 self.facades.clear()
178 for facade in info:
179 self.facades[facade['name']] = facade['versions'][-1]
180
181 async def login(self, username, password):
182 if not username.startswith('user-'):
183 username = 'user-{}'.format(username)
184
185 result = await self.rpc({
186 "type": "Admin",
187 "request": "Login",
188 "version": 3,
189 "params": {
190 "auth-tag": username,
191 "credentials": password,
192 "nonce": "".join(random.sample(string.printable, 12)),
193 }})
194 return result['response']
195
196
197 class JujuData:
198 def __init__(self):
199 self.path = os.environ.get('JUJU_DATA') or '~/.local/share/juju'
200 self.path = os.path.abspath(os.path.expanduser(self.path))
201
202 def current_controller(self):
203 cmd = shlex.split('juju show-controller --format yaml')
204 output = subprocess.check_output(cmd)
205 output = yaml.safe_load(output)
206 return list(output.keys())[0]
207
208 def controllers(self):
209 return self._load_yaml('controllers.yaml', 'controllers')
210
211 def models(self):
212 return self._load_yaml('models.yaml', 'controllers')
213
214 def accounts(self):
215 return self._load_yaml('accounts.yaml', 'controllers')
216
217 def _load_yaml(self, filename, key):
218 filepath = os.path.join(self.path, filename)
219 with io.open(filepath, 'rt') as f:
220 return yaml.safe_load(f)[key]