blob: 1cba79a2020b8e8b8fc79f5545f4c59afe87524c [file] [log] [blame]
Adam Israeldcdf82b2017-08-15 15:26:43 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import mock
Adam Israeldcdf82b2017-08-15 15:26:43 -04003from concurrent.futures import ThreadPoolExecutor
4from pathlib import Path
Adam Israelc3e6c2e2018-03-01 09:31:50 -05005
6from juju.client.client import ConfigValue, ApplicationFacade
7from juju.model import Model, ModelObserver
8from juju.utils import block_until, run_with_interrupt
Adam Israelb0943662018-08-02 15:32:00 -04009from juju.errors import JujuError
10
11import os
12import pylxd
13import time
14import uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -050015
Adam Israeldcdf82b2017-08-15 15:26:43 -040016import pytest
17
18from .. import base
Adam Israelc3e6c2e2018-03-01 09:31:50 -050019
Adam Israeldcdf82b2017-08-15 15:26:43 -040020
21MB = 1
22GB = 1024
23SSH_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsYMJGNGG74HAJha3n2CFmWYsOOaORnJK6VqNy86pj0MIpvRXBzFzVy09uPQ66GOQhTEoJHEqE77VMui7+62AcMXT+GG7cFHcnU8XVQsGM6UirCcNyWNysfiEMoAdZScJf/GvoY87tMEszhZIUV37z8PUBx6twIqMdr31W1J0IaPa+sV6FEDadeLaNTvancDcHK1zuKsL39jzAg7+LYjKJfEfrsQP+lj/EQcjtKqlhVS5kzsJVfx8ZEd0xhW5G7N6bCdKNalS8mKCMaBXJpijNQ82AiyqCIDCRrre2To0/i7pTjRiL0U9f9mV3S4NJaQaokR050w/ZLySFf6F7joJT mathijs@Qrama-Mathijs' # noqa
24
25
26@base.bootstrapped
27@pytest.mark.asyncio
28async def test_deploy_local_bundle(event_loop):
Adam Israeldcdf82b2017-08-15 15:26:43 -040029 tests_dir = Path(__file__).absolute().parent.parent
30 bundle_path = tests_dir / 'bundle'
Adam Israelc3e6c2e2018-03-01 09:31:50 -050031 mini_bundle_file_path = bundle_path / 'mini-bundle.yaml'
Adam Israeldcdf82b2017-08-15 15:26:43 -040032
33 async with base.CleanModel() as model:
34 await model.deploy(str(bundle_path))
Adam Israelc3e6c2e2018-03-01 09:31:50 -050035 await model.deploy(str(mini_bundle_file_path))
Adam Israeldcdf82b2017-08-15 15:26:43 -040036
Adam Israelc3e6c2e2018-03-01 09:31:50 -050037 for app in ('wordpress', 'mysql', 'myapp'):
Adam Israeldcdf82b2017-08-15 15:26:43 -040038 assert app in model.applications
39
40
41@base.bootstrapped
42@pytest.mark.asyncio
Adam Israelb0943662018-08-02 15:32:00 -040043async def test_deploy_invalid_bundle(event_loop):
44 tests_dir = Path(__file__).absolute().parent.parent
45 bundle_path = tests_dir / 'bundle' / 'invalid.yaml'
46 async with base.CleanModel() as model:
47 with pytest.raises(JujuError):
48 await model.deploy(str(bundle_path))
49
50
51@base.bootstrapped
52@pytest.mark.asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053async def test_deploy_local_charm(event_loop):
54 from pathlib import Path
55 tests_dir = Path(__file__).absolute().parent.parent
56 charm_path = tests_dir / 'charm'
57
58 async with base.CleanModel() as model:
59 await model.deploy(str(charm_path))
60 assert 'charm' in model.applications
61
62
63@base.bootstrapped
64@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -040065async def test_deploy_bundle(event_loop):
66 async with base.CleanModel() as model:
67 await model.deploy('bundle/wiki-simple')
68
69 for app in ('wiki', 'mysql'):
70 assert app in model.applications
71
72
73@base.bootstrapped
74@pytest.mark.asyncio
75async def test_deploy_channels_revs(event_loop):
76 async with base.CleanModel() as model:
77 charm = 'cs:~johnsca/libjuju-test'
78 stable = await model.deploy(charm, 'a1')
79 edge = await model.deploy(charm, 'a2', channel='edge')
Adam Israelc3e6c2e2018-03-01 09:31:50 -050080 rev = await model.deploy(charm + '-2', 'a3')
Adam Israeldcdf82b2017-08-15 15:26:43 -040081
82 assert [a.charm_url for a in (stable, edge, rev)] == [
83 'cs:~johnsca/libjuju-test-1',
84 'cs:~johnsca/libjuju-test-2',
85 'cs:~johnsca/libjuju-test-2',
86 ]
87
88
89@base.bootstrapped
90@pytest.mark.asyncio
91async def test_add_machine(event_loop):
92 from juju.machine import Machine
93
94 async with base.CleanModel() as model:
95 # add a new default machine
96 machine1 = await model.add_machine()
97
98 # add a machine with constraints, disks, and series
99 machine2 = await model.add_machine(
100 constraints={
101 'mem': 256 * MB,
102 },
103 disks=[{
104 'pool': 'rootfs',
105 'size': 10 * GB,
106 'count': 1,
107 }],
108 series='xenial',
109 )
110
111 # add a lxd container to machine2
112 machine3 = await model.add_machine(
113 'lxd:{}'.format(machine2.id))
114
115 for m in (machine1, machine2, machine3):
116 assert isinstance(m, Machine)
117
118 assert len(model.machines) == 3
119
120 await machine3.destroy(force=True)
121 await machine2.destroy(force=True)
122 res = await machine1.destroy(force=True)
123
124 assert res is None
125 assert len(model.machines) == 0
126
127
128@base.bootstrapped
129@pytest.mark.asyncio
Adam Israelb0943662018-08-02 15:32:00 -0400130async def test_add_manual_machine_ssh(event_loop):
131
132 # Verify controller is localhost
133 async with base.CleanController() as controller:
134 cloud = await controller.get_cloud()
135 if cloud != "localhost":
136 pytest.skip('Skipping because test requires lxd.')
137
138 async with base.CleanModel() as model:
139 private_key_path = os.path.expanduser(
140 "~/.local/share/juju/ssh/juju_id_rsa"
141 )
142 public_key_path = os.path.expanduser(
143 "~/.local/share/juju/ssh/juju_id_rsa.pub"
144 )
145
146 # Use the self-signed cert generated by lxc on first run
147 crt = os.path.expanduser('~/snap/lxd/current/.config/lxc/client.crt')
148 assert os.path.exists(crt)
149
150 key = os.path.expanduser('~/snap/lxd/current/.config/lxc/client.key')
151 assert os.path.exists(key)
152
153 client = pylxd.Client(
154 endpoint="https://127.0.0.1:8443",
155 cert=(crt, key),
156 verify=False,
157 )
158
159 test_name = "test-{}-add-manual-machine-ssh".format(
160 uuid.uuid4().hex[-4:]
161 )
162
163 # create profile w/cloud-init and juju ssh key
164 public_key = ""
165 with open(public_key_path, "r") as f:
166 public_key = f.readline()
167
168 profile = client.profiles.create(
169 test_name,
170 config={'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key)},
171 devices={
172 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
173 'eth0': {
174 'nictype': 'bridged',
175 'parent': 'lxdbr0',
176 'type': 'nic'
177 }
178 }
179 )
180
181 # create lxc machine
182 config = {
183 'name': test_name,
184 'source': {
185 'type': 'image',
186 'alias': 'xenial',
187 'mode': 'pull',
188 'protocol': 'simplestreams',
189 'server': 'https://cloud-images.ubuntu.com/releases',
190 },
191 'profiles': [test_name],
192 }
193 container = client.containers.create(config, wait=True)
194 container.start(wait=True)
195
196 def wait_for_network(container, timeout=30):
197 """Wait for eth0 to have an ipv4 address."""
198 starttime = time.time()
199 while(time.time() < starttime + timeout):
200 time.sleep(1)
201 if 'eth0' in container.state().network:
202 addresses = container.state().network['eth0']['addresses']
203 if len(addresses) > 0:
204 if addresses[0]['family'] == 'inet':
205 return addresses[0]
206 return None
207
208 host = wait_for_network(container)
209
210 # HACK: We need to give sshd a chance to bind to the interface,
211 # and pylxd's container.execute seems to be broken and fails and/or
212 # hangs trying to properly check if the service is up.
213 time.sleep(5)
214
215 if host:
216 # add a new manual machine
217 machine1 = await model.add_machine(spec='ssh:{}@{}:{}'.format(
218 "ubuntu",
219 host['address'],
220 private_key_path,
221 ))
222
223 assert len(model.machines) == 1
224
225 res = await machine1.destroy(force=True)
226
227 assert res is None
228 assert len(model.machines) == 0
229
230 container.stop(wait=True)
231 container.delete(wait=True)
232
233 profile.delete()
234
235
236@base.bootstrapped
237@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -0400238async def test_relate(event_loop):
239 from juju.relation import Relation
240
241 async with base.CleanModel() as model:
242 await model.deploy(
243 'ubuntu',
244 application_name='ubuntu',
245 series='trusty',
246 channel='stable',
247 )
248 await model.deploy(
249 'nrpe',
250 application_name='nrpe',
251 series='trusty',
252 channel='stable',
253 # subordinates must be deployed without units
254 num_units=0,
255 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500256
257 relation_added = asyncio.Event()
258 timeout = asyncio.Event()
259
260 class TestObserver(ModelObserver):
261 async def on_relation_add(self, delta, old, new, model):
262 if set(new.key.split()) == {'nrpe:general-info',
263 'ubuntu:juju-info'}:
264 relation_added.set()
265 event_loop.call_later(2, timeout.set)
266
267 model.add_observer(TestObserver())
268
269 real_app_facade = ApplicationFacade.from_connection(model.connection())
270 mock_app_facade = mock.MagicMock()
271
272 async def mock_AddRelation(*args):
273 # force response delay from AddRelation to test race condition
274 # (see https://github.com/juju/python-libjuju/issues/191)
275 result = await real_app_facade.AddRelation(*args)
276 await relation_added.wait()
277 return result
278
279 mock_app_facade.AddRelation = mock_AddRelation
280
281 with mock.patch.object(ApplicationFacade, 'from_connection',
282 return_value=mock_app_facade):
283 my_relation = await run_with_interrupt(model.add_relation(
284 'ubuntu',
285 'nrpe',
286 ), timeout, event_loop)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400287
288 assert isinstance(my_relation, Relation)
289
290
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500291async def _deploy_in_loop(new_loop, model_name, jujudata):
292 new_model = Model(new_loop, jujudata=jujudata)
293 await new_model.connect(model_name)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400294 try:
295 await new_model.deploy('cs:xenial/ubuntu')
296 assert 'ubuntu' in new_model.applications
297 finally:
298 await new_model.disconnect()
299
300
301@base.bootstrapped
302@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -0400303async def test_explicit_loop_threaded(event_loop):
304 async with base.CleanModel() as model:
305 model_name = model.info.name
306 new_loop = asyncio.new_event_loop()
307 with ThreadPoolExecutor(1) as executor:
308 f = executor.submit(
309 new_loop.run_until_complete,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500310 _deploy_in_loop(new_loop, model_name, model._connector.jujudata))
Adam Israeldcdf82b2017-08-15 15:26:43 -0400311 f.result()
312 await model._wait_for_new('application', 'ubuntu')
313 assert 'ubuntu' in model.applications
314
315
316@base.bootstrapped
317@pytest.mark.asyncio
318async def test_store_resources_charm(event_loop):
319 async with base.CleanModel() as model:
320 ghost = await model.deploy('cs:ghost-19')
321 assert 'ghost' in model.applications
322 terminal_statuses = ('active', 'error', 'blocked')
323 await model.block_until(
324 lambda: (
325 len(ghost.units) > 0 and
326 ghost.units[0].workload_status in terminal_statuses)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500327 )
Adam Israeldcdf82b2017-08-15 15:26:43 -0400328 # ghost will go in to blocked (or error, for older
329 # charm revs) if the resource is missing
330 assert ghost.units[0].workload_status == 'active'
331
332
333@base.bootstrapped
334@pytest.mark.asyncio
335async def test_store_resources_bundle(event_loop):
336 async with base.CleanModel() as model:
337 bundle = str(Path(__file__).parent / 'bundle')
338 await model.deploy(bundle)
339 assert 'ghost' in model.applications
340 ghost = model.applications['ghost']
341 terminal_statuses = ('active', 'error', 'blocked')
342 await model.block_until(
343 lambda: (
344 len(ghost.units) > 0 and
345 ghost.units[0].workload_status in terminal_statuses)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500346 )
Adam Israeldcdf82b2017-08-15 15:26:43 -0400347 # ghost will go in to blocked (or error, for older
348 # charm revs) if the resource is missing
349 assert ghost.units[0].workload_status == 'active'
350
351
352@base.bootstrapped
353@pytest.mark.asyncio
354async def test_ssh_key(event_loop):
355 async with base.CleanModel() as model:
356 await model.add_ssh_key('admin', SSH_KEY)
357 result = await model.get_ssh_key(True)
358 result = result.serialize()['results'][0].serialize()['result']
359 assert SSH_KEY in result
360 await model.remove_ssh_key('admin', SSH_KEY)
361 result = await model.get_ssh_key(True)
362 result = result.serialize()['results'][0].serialize()['result']
363 assert result is None
364
365
366@base.bootstrapped
367@pytest.mark.asyncio
368async def test_get_machines(event_loop):
369 async with base.CleanModel() as model:
370 result = await model.get_machines()
371 assert isinstance(result, list)
372
373
374@base.bootstrapped
375@pytest.mark.asyncio
376async def test_watcher_reconnect(event_loop):
377 async with base.CleanModel() as model:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500378 await model.connection().ws.close()
379 await block_until(model.is_connected, timeout=3)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400380
381
382@base.bootstrapped
383@pytest.mark.asyncio
384async def test_config(event_loop):
385 async with base.CleanModel() as model:
386 await model.set_config({
387 'extra-info': 'booyah',
388 'test-mode': ConfigValue(value=True),
389 })
390 result = await model.get_config()
391 assert 'extra-info' in result
392 assert result['extra-info'].source == 'model'
393 assert result['extra-info'].value == 'booyah'
394
Adam Israelb0943662018-08-02 15:32:00 -0400395@base.bootstrapped
396@pytest.mark.asyncio
397async def test_set_constraints(event_loop):
398 async with base.CleanModel() as model:
399 await model.set_constraints({'cpu-power': 1})
400 cons = await model.get_constraints()
401 assert cons['cpu_power'] == 1
402
Adam Israeldcdf82b2017-08-15 15:26:43 -0400403# @base.bootstrapped
404# @pytest.mark.asyncio
405# async def test_grant(event_loop)
406# async with base.CleanController() as controller:
407# await controller.add_user('test-model-grant')
408# await controller.grant('test-model-grant', 'superuser')
409# async with base.CleanModel() as model:
410# await model.grant('test-model-grant', 'admin')
411# assert model.get_user('test-model-grant')['access'] == 'admin'
412# await model.grant('test-model-grant', 'login')
413# assert model.get_user('test-model-grant')['access'] == 'login'