Merge lp:~gz/juju-ci-tools/endpoints_bindings into lp:juju-ci-tools

Proposed by Martin Packman
Status: Merged
Approved by: Martin Packman
Approved revision: 1660
Merged at revision: 1675
Proposed branch: lp:~gz/juju-ci-tools/endpoints_bindings
Merge into: lp:juju-ci-tools
Diff against target: 708 lines (+586/-6)
6 files modified
assess_endpoint_bindings.py (+344/-0)
jujupy.py (+23/-5)
substrate.py (+22/-0)
tests/test_assess_endpoint_bindings.py (+177/-0)
tests/test_jujupy.py (+2/-1)
tests/test_substrate.py (+18/-0)
To merge this branch: bzr merge lp:~gz/juju-ci-tools/endpoints_bindings
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+308923@code.launchpad.net

Description of the change

Work in progress on making endpoint bindings test that can be run on shared maas

Basic outline is that the networking parts are set up and torn down using the underlying maas commands each run, and that machines that need interfaces reconfigured are constrained from being used on other maas jobs. There are still some details here that need discussing in more depth, but the basic outline here should provide a start.

I have chopped out most of the test complexity that I was adding locally for ease of review. Also, had to make some changes to how the network configuration was done in light of how finfolk is actually set up, so for now have deleted the tests based on real data for the maas configuration, will update with the new behaviour and re-add.

There's a bunch more to talk about here as well, happy to answer any questions.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you. I have some advice, questions, and comments inline. Nothing blocks this branch from merging.

review: Approve (code)
Revision history for this message
Martin Packman (gz) wrote :

Thanks for all the comments, have replied in line and with push some changes.

1659. By Martin Packman

Address review comments from sinzui and use persistent spaces

1660. By Martin Packman

Give gateway_ip when creating subnets

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'assess_endpoint_bindings.py'
2--- assess_endpoint_bindings.py 1970-01-01 00:00:00 +0000
3+++ assess_endpoint_bindings.py 2016-10-21 22:18:30 +0000
4@@ -0,0 +1,344 @@
5+#!/usr/bin/env python
6+"""Validate endpoint bindings functionality on MAAS."""
7+
8+from __future__ import print_function
9+
10+import argparse
11+import contextlib
12+import logging
13+import os
14+import sys
15+import yaml
16+
17+from deploy_stack import (
18+ BootstrapManager,
19+)
20+from jujucharm import (
21+ Charm,
22+)
23+from substrate import (
24+ maas_account_from_config,
25+)
26+from utility import (
27+ add_basic_testing_arguments,
28+ configure_logging,
29+ temp_dir,
30+)
31+
32+
33+log = logging.getLogger("assess_endpoint_bindings")
34+
35+
36+script_identifier = "endpoint-bindings"
37+
38+# To avoid clashes with other tests these space names must be seperately
39+# registered in jujupy to populate constraints.
40+space_control = script_identifier + "-control"
41+space_data = script_identifier + "-data"
42+space_public = script_identifier + "-public"
43+
44+
45+def _generate_vids(start=10):
46+ """
47+ Generate a series of vid values beginning with start.
48+
49+ Ideally these values would be carefully chosen to not clash with existing
50+ vlans, but for now just hardcode.
51+ """
52+ for vid in range(start, 4096):
53+ yield vid
54+
55+
56+def _generate_cidrs(start=40, inc=10, block_pattern="10.0.{}.0/24"):
57+ """
58+ Generate a series of cidrs based on block_pattern beginning with start.
59+
60+ Would be good not to hardcode but inspecting network for free ranges is
61+ also non-trivial.
62+ """
63+ for n in range(start, 255, inc):
64+ yield block_pattern.format(n)
65+
66+
67+def ensure_spaces(manager, required_spaces):
68+ """Return details for each given required_spaces creating spaces as needed.
69+
70+ :param manager: MAAS account manager.
71+ :param required_spaces: List of space names that may need to be created.
72+ """
73+ existing_spaces = manager.spaces()
74+ log.info("Have spaces: %s", ", ".join(s["name"] for s in existing_spaces))
75+ spaces_map = dict((s["name"], s) for s in existing_spaces)
76+ spaces = []
77+ for space_name in required_spaces:
78+ space = spaces_map.get(space_name)
79+ if space is None:
80+ space = manager.create_space(space_name)
81+ log.info("Created space: %r", space)
82+ spaces.append(space)
83+ return spaces
84+
85+
86+@contextlib.contextmanager
87+def reconfigure_networking(manager, required_spaces):
88+ """Create new MAAS networking primitives to prepare for testing.
89+
90+ :param manager: MAAS account manager.
91+ :param required_spaces: List of spaces to make with vlans and subnets.
92+ """
93+ new_subnets = []
94+ new_vlans = []
95+ fabrics = manager.fabrics()
96+ log.info("Have fabrics: %s", ", ".join(f["name"] for f in fabrics))
97+ new_fabric = manager.create_fabric(script_identifier)
98+ try:
99+ log.info("Created fabric: %r", new_fabric)
100+
101+ spaces = ensure_spaces(manager, required_spaces)
102+
103+ for vid, space_name in zip(_generate_vids(), required_spaces):
104+ name = space_name + "-vlan"
105+ new_vlans.append(manager.create_vlan(new_fabric["id"], vid, name))
106+ log.info("Created vlan: %r", new_vlans[-1])
107+
108+ for cidr, vlan, space in zip(_generate_cidrs(), new_vlans, spaces):
109+ new_subnets.append(manager.create_subnet(
110+ cidr, fabric_id=new_fabric["id"], vlan_id=vlan["id"],
111+ space=space["id"], gateway_ip=cidr.replace(".0/24", ".1")))
112+ log.info("Created subnet: %r", new_subnets[-1])
113+
114+ yield new_fabric, spaces, list(new_vlans), list(new_subnets)
115+
116+ finally:
117+ for subnet in new_subnets:
118+ manager.delete_subnet(subnet["id"])
119+ log.info("Deleted subnet: %s", subnet["name"])
120+
121+ for vlan in new_vlans:
122+ manager.delete_vlan(new_fabric["id"], vlan["vid"])
123+ log.info("Deleted vlan: %s", vlan["name"])
124+
125+ try:
126+ manager.delete_fabric(new_fabric["id"])
127+ except Exception:
128+ log.exception("Failed to delete fabric: %s", new_fabric["id"])
129+ raise
130+ else:
131+ log.info("Deleted fabric: %s", new_fabric["id"])
132+
133+
134+@contextlib.contextmanager
135+def reconfigure_machines(manager, fabric, required_machine_subnets):
136+ """
137+ Reconfigure MAAS machines with new interfaces to prepare for testing.
138+
139+ There are some unavoidable races if multiple jobs attempt to reconfigure
140+ machines at the same time. Also, in heterogenous environments an
141+ inadequate machine may be reserved at this time.
142+
143+ Ideally this function would just allocate some machines before operating
144+ on them. Alas, MAAS doesn't allow interface changes on allocated machines
145+ and Juju will not select them for deployment.
146+
147+ :param manager: MAAS account manager.
148+ :param fabric: Data from MAAS about the fabric to be used.
149+ :param required_machine_subnets: List of list of vlan and subnet ids.
150+ """
151+
152+ # Find all machines not currently being used
153+ all_machines = manager.machines()
154+ candidate_machines = [
155+ m for m in all_machines if m["status"] == manager.STATUS_READY]
156+ # Take the id of the default vlan on the new fabric
157+ default_vlan = fabric["vlans"][0]["id"]
158+
159+ configured_machines = []
160+ machine_interfaces = {}
161+ try:
162+ for machine_subnets in required_machine_subnets:
163+ if not candidate_machines:
164+ raise Exception("No ready maas machines to configure")
165+
166+ machine = candidate_machines.pop()
167+ system_id = machine["system_id"]
168+ # TODO(gz): Add logic to pick sane parent?
169+ existing_interface = [
170+ interface for interface in machine["interface_set"]
171+ if not any("subnet" in link for link in interface["links"])
172+ ][0]
173+ previous_vlan_id = existing_interface["vlan"]["id"]
174+ new_interfaces = []
175+ machine_interfaces[system_id] = (
176+ existing_interface, previous_vlan_id, new_interfaces)
177+ manager.interface_update(
178+ system_id, existing_interface["id"], vlan_id=default_vlan)
179+ log.info("Changed existing interface: %s %s", system_id,
180+ existing_interface["name"])
181+ parent = existing_interface["id"]
182+
183+ for vlan_id, subnet_id in machine_subnets:
184+ links = []
185+ interface = manager.interface_create_vlan(
186+ system_id, parent, vlan_id)
187+ new_interfaces.append(interface)
188+ log.info("Created interface: %r", interface)
189+
190+ updated_subnet = manager.interface_link_subnet(
191+ system_id, interface["id"], "AUTO", subnet_id)
192+ # TODO(gz): Need to pick out right link if multiple are added.
193+ links.append(updated_subnet["links"][0])
194+ log.info("Created link: %r", links[-1])
195+
196+ configured_machines.append(machine)
197+ yield configured_machines
198+ finally:
199+ log.info("About to reset machine interfaces to original states.")
200+ for system_id in machine_interfaces:
201+ parent, vlan, children = machine_interfaces[system_id]
202+ for child in children:
203+ manager.delete_interface(system_id, child["id"])
204+ log.info("Deleted interface: %s %s", system_id, child["id"])
205+ manager.interface_update(system_id, parent["id"], vlan_id=vlan)
206+ log.info("Reset original interface: %s", parent["name"])
207+
208+
209+def create_test_charms():
210+ """Create charms for testing and bundle using them."""
211+ charm_datastore = Charm("datastore", "Testing datastore charm.")
212+ charm_datastore.metadata["provides"] = {
213+ "datastore": {"interface": "data"},
214+ }
215+
216+ charm_frontend = Charm("frontend", "Testing frontend charm.")
217+ charm_frontend.metadata["provides"] = {
218+ "website": {"interface": "http"},
219+ }
220+ charm_frontend.metadata["requires"] = {
221+ "datastore": {"interface": "data"},
222+ }
223+
224+ bundle = {
225+ "services": {
226+ "datastore": {
227+ "charm": "./xenial/datastore",
228+ "series": "xenial",
229+ "num-units": 1,
230+ "bindings": {
231+ "datastore": space_data,
232+ },
233+ },
234+ "frontend": {
235+ "charm": "./xenial/frontend",
236+ "series": "xenial",
237+ "num-units": 1,
238+ "bindings": {
239+ "website": space_public,
240+ "datastore": space_data,
241+ },
242+ },
243+ },
244+ "relations": [
245+ ["datastore:datastore", "frontend:datastore"],
246+ ],
247+ }
248+ return bundle, [charm_datastore, charm_frontend]
249+
250+
251+@contextlib.contextmanager
252+def using_bundle_and_charms(bundle, charms, bundle_name="bundle.yaml"):
253+ """Commit bundle and charms to disk and gives path to bundle."""
254+ with temp_dir() as working_dir:
255+ for charm in charms:
256+ charm.to_repo_dir(working_dir)
257+
258+ # TODO(gz): Create a bundle abstration in jujucharm module
259+ bundle_path = os.path.join(working_dir, bundle_name)
260+ with open(bundle_path, "w") as f:
261+ yaml.safe_dump(bundle, f)
262+
263+ yield bundle_path
264+
265+
266+def machine_spaces_for_bundle(bundle):
267+ """Return a list of sets of spaces required for machines in bundle."""
268+ machines = []
269+ for service in bundle["services"].values():
270+ spaces = frozenset(service.get("bindings", {}).values())
271+ num_units = service.get("num_units", 1)
272+ machines.extend([spaces] * num_units)
273+ return machines
274+
275+
276+def bootstrap_and_test(bootstrap_manager, bundle_path, machine):
277+ with bootstrap_manager.booted_context(False, to=machine):
278+ client = bootstrap_manager.client
279+ log.info("Deploying bundle.")
280+ client.deploy(bundle_path)
281+ log.info("Waiting for all units to start.")
282+ client.wait_for_started()
283+ client.wait_for_workloads()
284+ log.info("Validating bindings.")
285+ validate(client)
286+
287+
288+def validate(client):
289+ """Ensure relations are bound to the correct spaces."""
290+
291+
292+def assess_endpoint_bindings(maas_manager, bootstrap_manager):
293+ required_spaces = [space_data, space_public]
294+
295+ bundle, charms = create_test_charms()
296+
297+ machine_spaces = machine_spaces_for_bundle(bundle)
298+ # Add a bootstrap machine in all spaces
299+ machine_spaces.insert(0, frozenset().union(*machine_spaces))
300+
301+ log.info("About to write charms to disk.")
302+ with using_bundle_and_charms(bundle, charms) as bundle_path:
303+ log.info("About to reconfigure maas networking.")
304+ with reconfigure_networking(maas_manager, required_spaces) as nets:
305+
306+ fabric, spaces, vlans, subnets = nets
307+ # Derive the vlans and subnets that need to be added to machines
308+ vlan_subnet_per_machine = []
309+ for spaces in machine_spaces:
310+ idxs = sorted(required_spaces.index(space) for space in spaces)
311+ vlans_subnets = [
312+ (vlans[i]["id"], subnets[i]["id"]) for i in idxs]
313+ vlan_subnet_per_machine.append(vlans_subnets)
314+
315+ log.info("About to add new interfaces to machines.")
316+ with reconfigure_machines(
317+ maas_manager, fabric, vlan_subnet_per_machine) as machines:
318+
319+ bootstrap_manager.client.use_reserved_spaces(required_spaces)
320+
321+ base_machine = machines[0]["hostname"]
322+
323+ log.info("About to bootstrap.")
324+ bootstrap_and_test(
325+ bootstrap_manager, bundle_path, base_machine)
326+
327+
328+def parse_args(argv):
329+ """Parse all arguments."""
330+ parser = argparse.ArgumentParser(description="assess endpoint bindings")
331+ add_basic_testing_arguments(parser)
332+ args = parser.parse_args(argv)
333+ if args.upload_tools:
334+ parser.error("giving --upload-tools meaningless on 2.0 only test")
335+ return args
336+
337+
338+def main(argv=None):
339+ args = parse_args(argv)
340+ configure_logging(args.verbose)
341+ bs_manager = BootstrapManager.from_args(args)
342+ with maas_account_from_config(bs_manager.client.env.config) as account:
343+ assess_endpoint_bindings(account, bs_manager)
344+ return 0
345+
346+
347+if __name__ == '__main__':
348+ sys.exit(main())
349
350=== modified file 'jujupy.py'
351--- jujupy.py 2016-10-21 19:50:45 +0000
352+++ jujupy.py 2016-10-21 22:18:30 +0000
353@@ -940,6 +940,9 @@
354
355 controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
356
357+ reserved_spaces = frozenset([
358+ 'endpoint-bindings-data', 'endpoint-bindings-public'])
359+
360 @classmethod
361 def preferred_container(cls):
362 for container_type in [LXD_MACHINE, LXC_MACHINE]:
363@@ -1032,6 +1035,7 @@
364 feature_flags = self.feature_flags.intersection(cls.used_feature_flags)
365 backend = self._backend.clone(full_path, version, debug, feature_flags)
366 other = cls.from_backend(backend, env)
367+ other.excluded_spaces = set(self.excluded_spaces)
368 return other
369
370 @classmethod
371@@ -1108,6 +1112,7 @@
372 env.juju_home = get_juju_home()
373 else:
374 env.juju_home = juju_home
375+ self.excluded_spaces = set(self.reserved_spaces)
376
377 @property
378 def version(self):
379@@ -1141,6 +1146,12 @@
380 return self._backend.shell_environ(self.used_feature_flags,
381 self.env.juju_home)
382
383+ def use_reserved_spaces(self, spaces):
384+ """Allow machines in given spaces to be allocated and used."""
385+ if not self.reserved_spaces.issuperset(spaces):
386+ raise ValueError('Space not reserved: {}'.format(spaces))
387+ self.excluded_spaces.difference_update(spaces)
388+
389 def add_ssh_machines(self, machines):
390 for count, machine in enumerate(machines):
391 try:
392@@ -1163,11 +1174,7 @@
393 credential=None, auto_upgrade=False, metadata_source=None,
394 to=None, no_gui=False, agent_version=None):
395 """Return the bootstrap arguments for the substrate."""
396- if self.env.joyent:
397- # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
398- constraints = 'mem=2G cpu-cores=1'
399- else:
400- constraints = 'mem=2G'
401+ constraints = self._get_substrate_constraints()
402 cloud_region = self.get_cloud_region(self.env.get_cloud(),
403 self.env.get_region())
404 # Note cloud_region before controller name
405@@ -1523,6 +1530,10 @@
406 if self.env.joyent:
407 # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
408 return 'mem=2G cpu-cores=1'
409+ elif self.env.maas:
410+ # For now only maas support spaces in a meaningful way.
411+ return 'mem=2G spaces={}'.format( ','.join(
412+ '^' + space for space in sorted(self.excluded_spaces)))
413 else:
414 return 'mem=2G'
415
416@@ -2559,6 +2570,13 @@
417 else:
418 return unqualified_model_name(self.model_name)
419
420+ def _get_substrate_constraints(self):
421+ if self.env.joyent:
422+ # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
423+ return 'mem=2G cpu-cores=1'
424+ else:
425+ return 'mem=2G'
426+
427 def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
428 credential=None):
429 """Return the bootstrap arguments for the substrate."""
430
431=== modified file 'substrate.py'
432--- substrate.py 2016-09-28 12:51:00 +0000
433+++ substrate.py 2016-10-21 22:18:30 +0000
434@@ -440,6 +440,8 @@
435
436 _API_PATH = 'api/2.0/'
437
438+ STATUS_READY = 4
439+
440 SUBNET_CONNECTION_MODES = frozenset(('AUTO', 'DHCP', 'STATIC', 'LINK_UP'))
441
442 def __init__(self, profile, url, oauth):
443@@ -493,6 +495,10 @@
444 if v['ip_addresses']}
445 return ips
446
447+ def machines(self):
448+ """Return list of all machines."""
449+ return self._maas(self.profile, 'machines', 'read')
450+
451 def fabrics(self):
452 """Return list of all fabrics."""
453 return self._maas(self.profile, 'fabrics', 'read')
454@@ -538,6 +544,22 @@
455 """Return list of interfaces belonging to node with given system_id."""
456 return self._maas(self.profile, 'interfaces', 'read', system_id)
457
458+ def interface_update(self, system_id, interface_id, name=None,
459+ mac_address=None, tags=None, vlan_id=None):
460+ """Update fields of existing interface on node with given system_id."""
461+ args = [
462+ self.profile, 'interface', 'update', system_id, str(interface_id),
463+ ]
464+ if name is not None:
465+ args.append('name=' + name)
466+ if mac_address is not None:
467+ args.append('mac_address=' + mac_address)
468+ if tags is not None:
469+ args.append('tags=' + tags)
470+ if vlan_id is not None:
471+ args.append('vlan=' + str(vlan_id))
472+ return self._maas(*args)
473+
474 def interface_create_vlan(self, system_id, parent, vlan_id):
475 """Create a vlan interface on machine with given system_id."""
476 args = [
477
478=== added file 'tests/test_assess_endpoint_bindings.py'
479--- tests/test_assess_endpoint_bindings.py 1970-01-01 00:00:00 +0000
480+++ tests/test_assess_endpoint_bindings.py 2016-10-21 22:18:30 +0000
481@@ -0,0 +1,177 @@
482+"""Tests for assess_endpoint_bindings module."""
483+
484+import logging
485+from mock import Mock, patch
486+import StringIO
487+
488+from assess_endpoint_bindings import (
489+ assess_endpoint_bindings,
490+ ensure_spaces,
491+ parse_args,
492+ machine_spaces_for_bundle,
493+ main,
494+)
495+from tests import (
496+ parse_error,
497+ TestCase,
498+)
499+from tests.test_jujupy import fake_juju_client
500+
501+
502+class TestParseArgs(TestCase):
503+
504+ def test_common_args(self):
505+ args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
506+ self.assertEqual("an-env", args.env)
507+ self.assertEqual("/bin/juju", args.juju_bin)
508+ self.assertEqual("/tmp/logs", args.logs)
509+ self.assertEqual("an-env-mod", args.temp_env_name)
510+ self.assertEqual(False, args.debug)
511+
512+ def test_no_upload_tools(self):
513+ with parse_error(self) as fake_stderr:
514+ parse_args(["an-env", "/bin/juju", "--upload-tools"])
515+ self.assertIn(
516+ "error: giving --upload-tools meaningless on 2.0 only test",
517+ fake_stderr.getvalue())
518+
519+ def test_help(self):
520+ fake_stdout = StringIO.StringIO()
521+ with parse_error(self) as fake_stderr:
522+ with patch("sys.stdout", fake_stdout):
523+ parse_args(["--help"])
524+ self.assertEqual("", fake_stderr.getvalue())
525+ self.assertIn("endpoint bindings", fake_stdout.getvalue())
526+
527+
528+class TestEnsureSpaces(TestCase):
529+
530+ default_space = {
531+ "name": "Default space",
532+ "id": 0,
533+ "resource_uri": "/MAAS/api/2.0/spaces/0/",
534+ "subnets": [
535+ {
536+ "space": "Default space",
537+ "id": 2,
538+ },
539+ ],
540+ }
541+ alpha_space = {
542+ "name": "alpha-space",
543+ "id": 60,
544+ "resource_uri": "/MAAS/api/2.0/spaces/60/",
545+ "subnets": [],
546+ }
547+ beta_space = {
548+ "name": "beta-space",
549+ "id": 61,
550+ "resource_uri": "/MAAS/api/2.0/spaces/61/",
551+ "subnets": [],
552+ }
553+
554+ def test_all_existing(self):
555+ manager = Mock(spec=["spaces"])
556+ manager.spaces.return_value = [self.default_space, self.alpha_space]
557+ spaces = ensure_spaces(manager, ["alpha-space"])
558+ self.assertEqual(spaces, [self.alpha_space])
559+ manager.spaces.assert_called_once_with()
560+ self.assertEqual(
561+ "INFO Have spaces: Default space, alpha-space\n",
562+ self.log_stream.getvalue())
563+
564+ def test_some_existing(self):
565+ manager = Mock(spec=["create_space", "spaces"])
566+ manager.create_space.return_value = self.alpha_space
567+ manager.spaces.return_value = [self.default_space, self.beta_space]
568+ spaces = ensure_spaces(manager, ["alpha-space", "beta-space"])
569+ self.assertEqual(spaces, [self.alpha_space, self.beta_space])
570+ manager.spaces.assert_called_once_with()
571+ manager.create_space.assert_called_once_with("alpha-space")
572+ self.assertRegexpMatches(
573+ self.log_stream.getvalue(),
574+ r"^INFO Have spaces: Default space, beta-space\n"
575+ r"INFO Created space: \{.*\}\n$")
576+
577+
578+class TestMachineSpacesForBundle(TestCase):
579+
580+ def test_no_bindings(self):
581+ bundle_without_bindings = {
582+ "services": {
583+ "ubuntu": {
584+ "charm": "cs:ubuntu",
585+ "num_units": 3,
586+ },
587+ },
588+ }
589+ machines = machine_spaces_for_bundle(bundle_without_bindings)
590+ self.assertEqual(machines, [frozenset(), frozenset(), frozenset()])
591+
592+ def test_single_binding(self):
593+ bundle_without_bindings = {
594+ "services": {
595+ "anapp": {
596+ "charm": "./anapp",
597+ "series": "xenial",
598+ "num_units": 1,
599+ "bindings": {
600+ "website": "space-public",
601+ },
602+ },
603+ },
604+ }
605+ machines = machine_spaces_for_bundle(bundle_without_bindings)
606+ self.assertEqual(machines, [frozenset(["space-public"])])
607+
608+ def test_multiple_bindings(self):
609+ bundle_without_bindings = {
610+ "services": {
611+ "anapp": {
612+ "charm": "./anapp",
613+ "series": "xenial",
614+ "num_units": 1,
615+ "bindings": {
616+ "website": "space-public",
617+ "data": "space-data",
618+ "monitoring": "space-ctl",
619+ },
620+ },
621+ "adb": {
622+ "charm": "./adb",
623+ "series": "xenial",
624+ "num_units": 2,
625+ "bindings": {
626+ "data": "space-data",
627+ "monitoring": "space-ctl",
628+ },
629+ },
630+ },
631+ }
632+ machines = machine_spaces_for_bundle(bundle_without_bindings)
633+ app_spaces = frozenset(["space-data", "space-ctl", "space-public"])
634+ db_spaces = frozenset(["space-data", "space-ctl"])
635+ self.assertEqual(machines, [app_spaces, db_spaces, db_spaces])
636+
637+
638+class TestMain(TestCase):
639+
640+ def test_main(self):
641+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
642+ env = Mock(spec=["config"])
643+ client = Mock(spec=["env", "is_jes_enabled"])
644+ client.env = env
645+ with patch("assess_endpoint_bindings.configure_logging",
646+ autospec=True) as mock_cl:
647+ with patch("assess_endpoint_bindings.maas_account_from_config",
648+ autospec=True) as mock_ma:
649+ with patch("deploy_stack.client_from_config",
650+ return_value=client) as mock_c:
651+ with patch("assess_endpoint_bindings.assess_endpoint_bindings",
652+ autospec=True) as mock_assess:
653+ main(argv)
654+ mock_cl.assert_called_once_with(logging.DEBUG)
655+ mock_c.assert_called_once_with(
656+ "an-env", "/bin/juju", debug=False, soft_deadline=None)
657+ self.assertEqual(mock_ma.call_count, 1)
658+ self.assertEqual(mock_assess.call_count, 1)
659
660=== modified file 'tests/test_jujupy.py'
661--- tests/test_jujupy.py 2016-10-21 20:02:20 +0000
662+++ tests/test_jujupy.py 2016-10-21 22:18:30 +0000
663@@ -796,7 +796,8 @@
664 client.bootstrap()
665 mock.assert_called_with(
666 'bootstrap', (
667- '--constraints', 'mem=2G',
668+ '--constraints', 'mem=2G spaces=^endpoint_bindings_data,'
669+ '^endpoint_bindings_public',
670 'foo/asdf', 'maas',
671 '--config', config_file.name, '--default-model', 'maas',
672 '--agent-version', '2.0'),
673
674=== modified file 'tests/test_substrate.py'
675--- tests/test_substrate.py 2016-10-14 19:07:39 +0000
676+++ tests/test_substrate.py 2016-10-21 22:18:30 +0000
677@@ -877,6 +877,14 @@
678 ('maas', 'mas', 'machines', 'list-allocated'))
679 self.assertEqual({}, ips)
680
681+ def test_machines(self):
682+ account = self.get_account()
683+ with patch('subprocess.check_output', autospec=True,
684+ return_value='[]') as co_mock:
685+ machines = account.machines()
686+ co_mock.assert_called_once_with(('maas', 'mas', 'machines', 'read'))
687+ self.assertEqual([], machines)
688+
689 def test_fabrics(self):
690 account = self.get_account()
691 with patch('subprocess.check_output', autospec=True,
692@@ -967,6 +975,16 @@
693 'maas', 'mas', 'interfaces', 'read', 'node-xyz'))
694 self.assertEqual([], interfaces)
695
696+ def test_interface_update(self):
697+ account = self.get_account()
698+ with patch('subprocess.check_output', autospec=True,
699+ return_value='{"id": 10}') as co_mock:
700+ interface = account.interface_update('node-xyz', 10, vlan=5000)
701+ co_mock.assert_called_once_with((
702+ 'maas', 'mas', 'interface', 'update', 'node-xyz', '10',
703+ 'vlan=5000'))
704+ self.assertEqual({'id': 10}, interface)
705+
706 def test_interface_create_vlan(self):
707 account = self.get_account()
708 with patch('subprocess.check_output', autospec=True,

Subscribers

People subscribed via source and target branches