3 from concurrent
.futures
import ThreadPoolExecutor
4 from pathlib
import Path
7 from juju
.client
.client
import ConfigValue
, ApplicationFacade
8 from juju
.model
import Model
, ModelObserver
9 from juju
.utils
import block_until
, run_with_interrupt
10 from juju
.errors
import JujuError
24 SSH_KEY
= 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsYMJGNGG74HAJha3n2CFmWYsOOaORnJK6VqNy86pj0MIpvRXBzFzVy09uPQ66GOQhTEoJHEqE77VMui7+62AcMXT+GG7cFHcnU8XVQsGM6UirCcNyWNysfiEMoAdZScJf/GvoY87tMEszhZIUV37z8PUBx6twIqMdr31W1J0IaPa+sV6FEDadeLaNTvancDcHK1zuKsL39jzAg7+LYjKJfEfrsQP+lj/EQcjtKqlhVS5kzsJVfx8ZEd0xhW5G7N6bCdKNalS8mKCMaBXJpijNQ82AiyqCIDCRrre2To0/i7pTjRiL0U9f9mV3S4NJaQaokR050w/ZLySFf6F7joJT mathijs@Qrama-Mathijs' # noqa
29 async def test_deploy_local_bundle_dir(event_loop
):
30 tests_dir
= Path(__file__
).absolute().parent
.parent
31 bundle_path
= tests_dir
/ 'bundle'
32 mini_bundle_file_path
= bundle_path
/ 'mini-bundle.yaml'
34 async with base
.CleanModel() as model
:
35 await model
.deploy(str(bundle_path
))
36 await model
.deploy(str(mini_bundle_file_path
))
38 wordpress
= model
.applications
.get('wordpress')
39 mysql
= model
.applications
.get('mysql')
40 assert wordpress
and mysql
41 await block_until(lambda: (len(wordpress
.units
) == 1 and
42 len(mysql
.units
) == 1),
48 async def test_deploy_local_bundle_file(event_loop
):
49 tests_dir
= Path(__file__
).absolute().parent
.parent
50 bundle_path
= tests_dir
/ 'bundle'
51 mini_bundle_file_path
= bundle_path
/ 'mini-bundle.yaml'
53 async with base
.CleanModel() as model
:
54 await model
.deploy(str(mini_bundle_file_path
))
56 dummy_sink
= model
.applications
.get('dummy-sink')
57 dummy_subordinate
= model
.applications
.get('dummy-subordinate')
58 assert dummy_sink
and dummy_subordinate
59 await block_until(lambda: (len(dummy_sink
.units
) == 1 and
60 len(dummy_subordinate
.units
) == 1),
66 async def test_deploy_invalid_bundle(event_loop
):
67 tests_dir
= Path(__file__
).absolute().parent
.parent
68 bundle_path
= tests_dir
/ 'bundle' / 'invalid.yaml'
69 async with base
.CleanModel() as model
:
70 with pytest
.raises(JujuError
):
71 await model
.deploy(str(bundle_path
))
76 async def test_deploy_local_charm(event_loop
):
77 from pathlib
import Path
78 tests_dir
= Path(__file__
).absolute().parent
.parent
79 charm_path
= tests_dir
/ 'charm'
81 async with base
.CleanModel() as model
:
82 await model
.deploy(str(charm_path
))
83 assert 'charm' in model
.applications
88 async def test_deploy_invalid_bundle(event_loop
):
89 tests_dir
= Path(__file__
).absolute().parent
.parent
90 bundle_path
= tests_dir
/ 'bundle' / 'invalid.yaml'
91 async with base
.CleanModel() as model
:
92 with pytest
.raises(JujuError
):
93 await model
.deploy(str(bundle_path
))
98 async def test_deploy_local_charm(event_loop
):
99 from pathlib
import Path
100 tests_dir
= Path(__file__
).absolute().parent
.parent
101 charm_path
= tests_dir
/ 'charm'
103 async with base
.CleanModel() as model
:
104 await model
.deploy(str(charm_path
))
105 assert 'charm' in model
.applications
110 async def test_deploy_bundle(event_loop
):
111 async with base
.CleanModel() as model
:
112 await model
.deploy('bundle/wiki-simple')
114 for app
in ('wiki', 'mysql'):
115 assert app
in model
.applications
120 async def test_deploy_channels_revs(event_loop
):
121 async with base
.CleanModel() as model
:
122 charm
= 'cs:~johnsca/libjuju-test'
123 stable
= await model
.deploy(charm
, 'a1')
124 edge
= await model
.deploy(charm
, 'a2', channel
='edge')
125 rev
= await model
.deploy(charm
+ '-2', 'a3')
127 assert [a
.charm_url
for a
in (stable
, edge
, rev
)] == [
128 'cs:~johnsca/libjuju-test-1',
129 'cs:~johnsca/libjuju-test-2',
130 'cs:~johnsca/libjuju-test-2',
136 async def test_add_machine(event_loop
):
137 from juju
.machine
import Machine
139 async with base
.CleanModel() as model
:
140 # add a new default machine
141 machine1
= await model
.add_machine()
143 # add a machine with constraints, disks, and series
144 machine2
= await model
.add_machine(
156 # add a lxd container to machine2
157 machine3
= await model
.add_machine(
158 'lxd:{}'.format(machine2
.id))
160 for m
in (machine1
, machine2
, machine3
):
161 assert isinstance(m
, Machine
)
163 assert len(model
.machines
) == 3
165 await machine3
.destroy(force
=True)
166 await machine2
.destroy(force
=True)
167 res
= await machine1
.destroy(force
=True)
170 assert len(model
.machines
) == 0
175 async def test_add_manual_machine_ssh(event_loop
):
177 # Verify controller is localhost
178 async with base
.CleanController() as controller
:
179 cloud
= await controller
.get_cloud()
180 if cloud
!= "localhost":
181 pytest
.skip('Skipping because test requires lxd.')
183 async with base
.CleanModel() as model
:
184 private_key_path
= os
.path
.expanduser(
185 "~/.local/share/juju/ssh/juju_id_rsa"
187 public_key_path
= os
.path
.expanduser(
188 "~/.local/share/juju/ssh/juju_id_rsa.pub"
191 # connect using the local unix socket
192 client
= pylxd
.Client()
194 test_name
= "test-{}-add-manual-machine-ssh".format(
195 uuid
.uuid4().hex[-4:]
198 # create profile w/cloud-init and juju ssh key
200 with
open(public_key_path
, "r") as f
:
201 public_key
= f
.readline()
203 profile
= client
.profiles
.create(
205 config
={'user.user-data': '#cloud-config\n'
206 'ssh_authorized_keys:\n'
207 '- {}'.format(public_key
)},
209 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
211 'nictype': 'bridged',
225 'protocol': 'simplestreams',
226 'server': 'https://cloud-images.ubuntu.com/releases',
228 'profiles': [test_name
],
230 container
= client
.containers
.create(config
, wait
=True)
231 container
.start(wait
=True)
233 def wait_for_network(container
, timeout
=30):
234 """Wait for eth0 to have an ipv4 address."""
235 starttime
= time
.time()
236 while(time
.time() < starttime
+ timeout
):
238 if 'eth0' in container
.state().network
:
239 addresses
= container
.state().network
['eth0']['addresses']
240 if len(addresses
) > 0:
241 if addresses
[0]['family'] == 'inet':
245 host
= wait_for_network(container
)
246 assert host
, 'Failed to get address for machine'
248 # HACK: We need to give sshd a chance to bind to the interface,
249 # and pylxd's container.execute seems to be broken and fails and/or
250 # hangs trying to properly check if the service is up.
253 for attempt
in range(1, 4):
255 # add a new manual machine
256 machine1
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
261 except paramiko
.ssh_exception
.NoValidConnectionsError
:
264 # retry the ssh connection a few times if it fails
265 time
.sleep(attempt
* 5)
269 assert len(model
.machines
) == 1
271 res
= await machine1
.destroy(force
=True)
274 assert len(model
.machines
) == 0
276 container
.stop(wait
=True)
277 container
.delete(wait
=True)
284 async def test_relate(event_loop
):
285 from juju
.relation
import Relation
287 async with base
.CleanModel() as model
:
290 application_name
='ubuntu',
296 application_name
='nrpe',
299 # subordinates must be deployed without units
303 relation_added
= asyncio
.Event()
304 timeout
= asyncio
.Event()
306 class TestObserver(ModelObserver
):
307 async def on_relation_add(self
, delta
, old
, new
, model
):
308 if set(new
.key
.split()) == {'nrpe:general-info',
311 event_loop
.call_later(2, timeout
.set)
313 model
.add_observer(TestObserver())
315 real_app_facade
= ApplicationFacade
.from_connection(model
.connection())
316 mock_app_facade
= mock
.MagicMock()
318 async def mock_AddRelation(*args
):
319 # force response delay from AddRelation to test race condition
320 # (see https://github.com/juju/python-libjuju/issues/191)
321 result
= await real_app_facade
.AddRelation(*args
)
322 await relation_added
.wait()
325 mock_app_facade
.AddRelation
= mock_AddRelation
327 with mock
.patch
.object(ApplicationFacade
, 'from_connection',
328 return_value
=mock_app_facade
):
329 my_relation
= await run_with_interrupt(model
.add_relation(
332 ), timeout
, loop
=event_loop
)
334 assert isinstance(my_relation
, Relation
)
337 async def _deploy_in_loop(new_loop
, model_name
, jujudata
):
338 new_model
= Model(new_loop
, jujudata
=jujudata
)
339 await new_model
.connect(model_name
)
341 await new_model
.deploy('cs:xenial/ubuntu')
342 assert 'ubuntu' in new_model
.applications
344 await new_model
.disconnect()
349 async def test_explicit_loop_threaded(event_loop
):
350 async with base
.CleanModel() as model
:
351 model_name
= model
.info
.name
352 new_loop
= asyncio
.new_event_loop()
353 with
ThreadPoolExecutor(1) as executor
:
355 new_loop
.run_until_complete
,
356 _deploy_in_loop(new_loop
,
358 model
._connector
.jujudata
))
360 await model
._wait
_for
_new
('application', 'ubuntu')
361 assert 'ubuntu' in model
.applications
366 async def test_store_resources_charm(event_loop
):
367 async with base
.CleanModel() as model
:
368 ghost
= await model
.deploy('cs:ghost-19')
369 assert 'ghost' in model
.applications
370 terminal_statuses
= ('active', 'error', 'blocked')
371 await model
.block_until(
373 len(ghost
.units
) > 0 and
374 ghost
.units
[0].workload_status
in terminal_statuses
)
376 # ghost will go in to blocked (or error, for older
377 # charm revs) if the resource is missing
378 assert ghost
.units
[0].workload_status
== 'active'
383 async def test_store_resources_bundle(event_loop
):
384 async with base
.CleanModel() as model
:
385 bundle
= str(Path(__file__
).parent
/ 'bundle')
386 await model
.deploy(bundle
)
387 assert 'ghost' in model
.applications
388 ghost
= model
.applications
['ghost']
389 terminal_statuses
= ('active', 'error', 'blocked')
390 await model
.block_until(
392 len(ghost
.units
) > 0 and
393 ghost
.units
[0].workload_status
in terminal_statuses
)
395 # ghost will go in to blocked (or error, for older
396 # charm revs) if the resource is missing
397 assert ghost
.units
[0].workload_status
== 'active'
398 resources
= await ghost
.get_resources()
399 assert resources
['ghost-stable'].revision
>= 12
404 async def test_store_resources_bundle_revs(event_loop
):
405 async with base
.CleanModel() as model
:
406 bundle
= str(Path(__file__
).parent
/ 'bundle/bundle-resource-rev.yaml')
407 await model
.deploy(bundle
)
408 assert 'ghost' in model
.applications
409 ghost
= model
.applications
['ghost']
410 terminal_statuses
= ('active', 'error', 'blocked')
411 await model
.block_until(
413 len(ghost
.units
) > 0 and
414 ghost
.units
[0].workload_status
in terminal_statuses
)
416 # ghost will go in to blocked (or error, for older
417 # charm revs) if the resource is missing
418 assert ghost
.units
[0].workload_status
== 'active'
419 resources
= await ghost
.get_resources()
420 assert resources
['ghost-stable'].revision
== 11
425 async def test_ssh_key(event_loop
):
426 async with base
.CleanModel() as model
:
427 await model
.add_ssh_key('admin', SSH_KEY
)
428 result
= await model
.get_ssh_key(True)
429 result
= result
.serialize()['results'][0].serialize()['result']
430 assert SSH_KEY
in result
431 await model
.remove_ssh_key('admin', SSH_KEY
)
432 result
= await model
.get_ssh_key(True)
433 result
= result
.serialize()['results'][0].serialize()['result']
434 assert result
is None
439 async def test_get_machines(event_loop
):
440 async with base
.CleanModel() as model
:
441 result
= await model
.get_machines()
442 assert isinstance(result
, list)
447 async def test_watcher_reconnect(event_loop
):
448 async with base
.CleanModel() as model
:
449 await model
.connection().ws
.close()
450 await block_until(model
.is_connected
, timeout
=3)
455 async def test_config(event_loop
):
456 async with base
.CleanModel() as model
:
457 await model
.set_config({
458 'extra-info': 'booyah',
459 'test-mode': ConfigValue(value
=True),
461 result
= await model
.get_config()
462 assert 'extra-info' in result
463 assert result
['extra-info'].source
== 'model'
464 assert result
['extra-info'].value
== 'booyah'
469 async def test_set_constraints(event_loop
):
470 async with base
.CleanModel() as model
:
471 await model
.set_constraints({'cpu-power': 1})
472 cons
= await model
.get_constraints()
473 assert cons
['cpu_power'] == 1
476 # @pytest.mark.asyncio
477 # async def test_grant(event_loop)
478 # async with base.CleanController() as controller:
479 # await controller.add_user('test-model-grant')
480 # await controller.grant('test-model-grant', 'superuser')
481 # async with base.CleanModel() as model:
482 # await model.grant('test-model-grant', 'admin')
483 # assert model.get_user('test-model-grant')['access'] == 'admin'
484 # await model.grant('test-model-grant', 'login')
485 # assert model.get_user('test-model-grant')['access'] == 'login'