From 92fed6f0c47a01eb5be9d6d0f462578af0bbe128 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Thu, 21 Dec 2017 13:41:03 +0100 Subject: [PATCH 01/31] allow passing dry_run to individual tasks --- brigade/core/__init__.py | 19 +++++++------- brigade/core/task.py | 2 +- tests/inventory_data/nsot/nsot.sqlite3 | Bin 220160 -> 228352 bytes tests/plugins/tasks/files/test_sftp.py | 24 +++++++++--------- .../tasks/networking/test_napalm_configure.py | 10 +++++--- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index d92400e6..2a2dfbfc 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -89,13 +89,13 @@ def filter(self, **kwargs): b.inventory = self.inventory.filter(**kwargs) return b - def _run_serial(self, task, **kwargs): + def _run_serial(self, task, dry_run, **kwargs): t = Task(task, **kwargs) result = AggregatedResult() for host in self.inventory.hosts.values(): try: logger.debug("{}: running task {}".format(host.name, t)) - r = t._start(host=host, brigade=self, dry_run=self.dry_run) + r = t._start(host=host, brigade=self, dry_run=dry_run) result[host.name] = r except Exception as e: logger.error("{}: {}".format(host, e)) @@ -103,11 +103,11 @@ def _run_serial(self, task, **kwargs): result.tracebacks[host.name] = traceback.format_exc() return result - def _run_parallel(self, task, num_workers, **kwargs): + def _run_parallel(self, task, num_workers, dry_run, **kwargs): result = AggregatedResult() pool = Pool(processes=num_workers) - result_pool = [pool.apply_async(run_task, args=(h, self, Task(task, **kwargs))) + result_pool = [pool.apply_async(run_task, args=(h, self, dry_run, Task(task, **kwargs))) for h in self.inventory.hosts.values()] pool.close() pool.join() @@ -121,7 +121,7 @@ def _run_parallel(self, task, num_workers, **kwargs): result[host] = res return result - def run(self, task, num_workers=None, **kwargs): + def run(self, task, num_workers=None, dry_run=None, **kwargs): """ Run task over all the hosts in the inventory. @@ -129,6 +129,7 @@ def run(self, task, num_workers=None, **kwargs): task (``callable``): function or callable that will be run against each device in the inventory num_workers(``int``): Override for how many hosts to run in paralell for this task + dry_run(``bool``): Whether if we are testing the changes or not **kwargs: additional argument to pass to ``task`` when calling it Raises: @@ -141,19 +142,19 @@ def run(self, task, num_workers=None, **kwargs): num_workers = num_workers or self.config.num_workers if num_workers == 1: - result = self._run_serial(task, **kwargs) + result = self._run_serial(task, dry_run, **kwargs) else: - result = self._run_parallel(task, num_workers, **kwargs) + result = self._run_parallel(task, num_workers, dry_run, **kwargs) if self.config.raise_on_error: result.raise_on_error() return result -def run_task(host, brigade, task): +def run_task(host, brigade, dry_run, task): try: logger.debug("{}: running task {}".format(host.name, task)) - r = task._start(host=host, brigade=brigade, dry_run=brigade.dry_run) + r = task._start(host=host, brigade=brigade, dry_run=dry_run) return host.name, r, None, None except Exception as e: logger.error("{}: {}".format(host, e)) diff --git a/brigade/core/task.py b/brigade/core/task.py index eee86b06..72a1e694 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -35,7 +35,7 @@ def __repr__(self): def _start(self, host, brigade, dry_run): self.host = host self.brigade = brigade - self.dry_run = dry_run + self.dry_run = dry_run if dry_run is not None else brigade.dry_run return self.task(self, **self.params) def run(self, task, **kwargs): diff --git a/tests/inventory_data/nsot/nsot.sqlite3 b/tests/inventory_data/nsot/nsot.sqlite3 index a1217f69ee7103d2e7c960baf0f341ea4340190d..5fd8479baee4161f562baafe6f0233a43985d474 100644 GIT binary patch delta 3206 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 cX5QaOMZ6p!rx9 Date: Thu, 21 Dec 2017 14:05:31 +0100 Subject: [PATCH 02/31] added write task --- brigade/plugins/tasks/files/__init__.py | 2 + brigade/plugins/tasks/files/write.py | 48 +++++++ tests/plugins/tasks/files/test_write.py | 158 ++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 brigade/plugins/tasks/files/write.py create mode 100644 tests/plugins/tasks/files/test_write.py diff --git a/brigade/plugins/tasks/files/__init__.py b/brigade/plugins/tasks/files/__init__.py index 1a430fa2..5c393eef 100644 --- a/brigade/plugins/tasks/files/__init__.py +++ b/brigade/plugins/tasks/files/__init__.py @@ -1,6 +1,8 @@ from .sftp import sftp +from .write import write __all__ = ( "sftp", + "write", ) diff --git a/brigade/plugins/tasks/files/write.py b/brigade/plugins/tasks/files/write.py new file mode 100644 index 00000000..6009e28a --- /dev/null +++ b/brigade/plugins/tasks/files/write.py @@ -0,0 +1,48 @@ +import difflib +import os + +from brigade.core.task import Result + + +def read_file(file): + if not os.path.exists(file): + return [] + with open(file, "r") as f: + return f.read().splitlines() + + +def generate_diff(filename, content, append): + original = read_file(filename) + if append: + c = list(original) + c.extend(content.splitlines()) + content = c + else: + content = content.splitlines() + + diff = difflib.unified_diff(original, content, fromfile=filename, tofile="new") + + return "\n".join(diff) + + +def write(task, filename, content, append=False): + """ + Write contents to a file (locally) + + Arguments: + filename (``str``): file you want to write into + conteint (``str``): content you want to write + append (``bool``): whether you want to replace the contents or append to it + + Returns: + * changed (``bool``): + * diff (``str``): unified diff + """ + diff = generate_diff(filename, content, append) + + if not task.dry_run: + mode = "a+" if append else "w+" + with open(filename, mode=mode) as f: + f.write(content) + + return Result(host=task.host, diff=diff, changed=bool(diff)) diff --git a/tests/plugins/tasks/files/test_write.py b/tests/plugins/tasks/files/test_write.py new file mode 100644 index 00000000..79584369 --- /dev/null +++ b/tests/plugins/tasks/files/test_write.py @@ -0,0 +1,158 @@ +import os +import uuid + +from brigade.plugins.tasks import files + + +content_a = """ +BLAH +BLEH +BLIH +BLOH +BLUH +""" + +content_b = """ +BLAH +BLOH +BLUH BLUH +BLIH +""" + + +diff_new = """--- /tmp/brigade-write/dev3.group_2-f66d9331-3eeb-4912-98b9-37f55ac48deb + ++++ new + +@@ -0,0 +1,6 @@ + ++ ++BLAH ++BLEH ++BLIH ++BLOH ++BLUH""" + +diff_overwrite = """--- /tmp/brigade-write/dev4.group_2-e63969eb-2261-4200-8913-196a12f4d791 + ++++ new + +@@ -1,6 +1,5 @@ + + + BLAH +-BLEH ++BLOH ++BLUH BLUH + BLIH +-BLOH +-BLUH""" # noqa + + +diff_append = """--- /tmp/brigade-write/dev4.group_2-36ea350d-6623-4098-a961-fc143504eb42 + ++++ new + +@@ -4,3 +4,8 @@ + + BLIH + BLOH + BLUH ++ ++BLAH ++BLOH ++BLUH BLUH ++BLIH""" # noqa + + +BASEPATH = "/tmp/brigade-write" +if not os.path.exists(BASEPATH): + os.makedirs(BASEPATH) + + +def _test_write(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + r = task.run(files.write, + dry_run=True, + filename=filename, + content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_a) + + assert not r.diff + assert not r.changed + + +def _test_overwrite(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_b) + + assert r.diff.splitlines()[1:] == diff_overwrite.splitlines()[1:] + assert r.changed + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_b) + + assert not r.diff + assert not r.changed + + +def _test_append(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write, + dry_run=False, + filename=filename, + content=content_b, + append=True) + + assert r.diff.splitlines()[1:] == diff_append.splitlines()[1:] + assert r.changed + + +class Test(object): + + def test_write(self, brigade): + brigade.run(_test_write) + + def test_overwrite(self, brigade): + brigade.run(_test_overwrite) + + def test_append(self, brigade): + brigade.run(_test_append) From 29f4c561aa51844355f38e5efbadf709deafbe5e Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 24 Dec 2017 16:39:44 +0100 Subject: [PATCH 03/31] redundant --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 1bde5d19..8e0b0df3 100644 --- a/.gitignore +++ b/.gitignore @@ -96,8 +96,4 @@ tags .vars output/ -demo/log - -tests.log - .DS_Store From 2a926dfb7c168c1f3d481eceedf9e9c8899a0fbd Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 24 Dec 2017 16:39:58 +0100 Subject: [PATCH 04/31] log by default to ./brigade.log --- brigade/core/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 2a2dfbfc..92e82c4c 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -72,6 +72,7 @@ def __init__(self, inventory, dry_run, config=None, config_file=None, logging.basicConfig( level=logging.ERROR, format=format, + filename="brigade.log", ) if available_connections is not None: self.available_connections = available_connections From b0ca3e04deceabe523afe2445c718654cb42c9f3 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 24 Dec 2017 16:40:20 +0100 Subject: [PATCH 05/31] raise an error properly if we fail to connect to the device --- brigade/core/inventory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index 52c5caf9..a0481615 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -217,7 +217,9 @@ def get_connection(self, connection): # the given host. We also have to set `num_workers=1` because chances are # we are already inside a thread # Task should establish a connection and populate self.connection[connection] - self.brigade.filter(name=self.name).run(conn_task, num_workers=1) + r = self.brigade.filter(name=self.name).run(conn_task, num_workers=1) + if self.name in r.failed_hosts: + raise r.failed_hosts[self.name] return self.connections[connection] From 80631867cc99a88243c435b779850eb494cf7826 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 24 Dec 2017 16:40:56 +0100 Subject: [PATCH 06/31] added more devices to the lab --- examples/Vagrantfile | 39 ++++++++--- examples/groups.yaml | 12 ---- examples/hosts.yaml | 110 +++++++++++++++++++++++-------- examples/network_diagram.graffle | Bin 0 -> 2241 bytes examples/network_diagram.png | Bin 0 -> 114423 bytes 5 files changed, 110 insertions(+), 51 deletions(-) create mode 100644 examples/network_diagram.graffle create mode 100644 examples/network_diagram.png diff --git a/examples/Vagrantfile b/examples/Vagrantfile index bb2bcabb..d576a0d7 100644 --- a/examples/Vagrantfile +++ b/examples/Vagrantfile @@ -11,22 +11,41 @@ You will need the boxes: Vagrant.configure(2) do |config| config.vbguest.auto_update = false - config.vm.define "eos" do |eos| - eos.vm.box = "vEOS-lab-4.17.5M" + config.vm.define "spine00" do |spine00| + spine00.vm.box = "vEOS-lab-4.17.5M" - eos.vm.network :forwarded_port, guest: 443, host: 12443, id: 'https' + spine00.vm.network :forwarded_port, guest: 443, host: 12444, id: 'https' - eos.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false - eos.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false + spine00.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false + spine00.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false end - config.vm.define "junos" do |junos| - junos.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" + config.vm.define "spine01" do |spine01| + spine01.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" - junos.vm.network :forwarded_port, guest: 22, host: 12203, id: 'ssh' + spine01.vm.network :forwarded_port, guest: 22, host: 12204, id: 'ssh' - junos.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false - junos.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false + spine01.vm.network "private_network", virtualbox__intnet: "link_3", ip: "169.254.1.11", auto_config: false + spine01.vm.network "private_network", virtualbox__intnet: "link_4", ip: "169.254.1.11", auto_config: false + end + + + config.vm.define "leaf00" do |leaf00| + leaf00.vm.box = "vEOS-lab-4.17.5M" + + leaf00.vm.network :forwarded_port, guest: 443, host: 12443, id: 'https' + + leaf00.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false + leaf00.vm.network "private_network", virtualbox__intnet: "link_3", ip: "169.254.1.11", auto_config: false + end + + config.vm.define "leaf01" do |leaf01| + leaf01.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" + + leaf01.vm.network :forwarded_port, guest: 22, host: 12203, id: 'ssh' + + leaf01.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false + leaf01.vm.network "private_network", virtualbox__intnet: "link_4", ip: "169.254.1.11", auto_config: false end end diff --git a/examples/groups.yaml b/examples/groups.yaml index 54ce9cd4..0bfa38c7 100644 --- a/examples/groups.yaml +++ b/examples/groups.yaml @@ -3,20 +3,8 @@ all: group: null domain: acme.com -bma-leaf: - group: bma - -bma-host: - group: bma - bma: group: all -cmh-leaf: - group: cmh - -cmh-host: - group: cmh - cmh: group: all diff --git a/examples/hosts.yaml b/examples/hosts.yaml index 0a1b80db..c78cdb80 100644 --- a/examples/hosts.yaml +++ b/examples/hosts.yaml @@ -2,63 +2,115 @@ host1.cmh: site: cmh role: host - group: cmh-host - nos: linux + group: cmh + brigade_nos: linux + type: host host2.cmh: site: cmh role: host - group: cmh-host - nos: linux + group: cmh + brigade_nos: linux + type: host -switch00.cmh: - brigade_ip: 127.0.0.1 +spine00.cmh: + brigade_host: 127.0.0.1 brigade_username: vagrant brigade_password: vagrant - napalm_port: 12443 + brigade_network_api_port: 12444 + site: cmh + role: spine + group: cmh + brigade_nos: eos + type: network_device + +spine01.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: "" + brigade_network_api_port: 12204 + site: cmh + role: spine + group: cmh + brigade_nos: junos + type: network_device + +leaf00.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12443 site: cmh role: leaf - group: cmh-leaf - nos: eos + group: cmh + brigade_nos: eos + type: network_device -switch01.cmh: - brigade_ip: 127.0.0.1 +leaf01.cmh: + brigade_host: 127.0.0.1 brigade_username: vagrant brigade_password: "" - napalm_port: 12203 + brigade_network_api_port: 12203 site: cmh role: leaf - group: cmh-leaf - nos: junos + group: cmh + brigade_nos: junos + type: network_device host1.bma: site: bma role: host - group: bma-host - nos: linux + group: bma + brigade_nos: linux + type: host host2.bma: site: bma role: host - group: bma-host - nos: linux + group: bma + brigade_nos: linux + type: host -switch00.bma: - brigade_ip: 127.0.0.1 +spine00.bma: + brigade_host: 127.0.0.1 brigade_username: vagrant brigade_password: vagrant - napalm_port: 12443 + brigade_network_api_port: 12444 site: bma - role: leaf - group: bma-leaf - nos: eos + role: spine + group: bma + brigade_nos: eos + type: network_device -switch01.bma: - brigade_ip: 127.0.0.1 +spine01.bma: + brigade_host: 127.0.0.1 brigade_username: vagrant brigade_password: "" - napalm_port: 12203 + brigade_network_api_port: 12204 + site: bma + role: spine + group: bma + brigade_nos: junos + type: network_device + +leaf00.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12443 + site: bma + role: leaf + group: bma + brigade_nos: eos + type: network_device + +leaf01.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: wrong_password + brigade_network_api_port: 12203 site: bma role: leaf - group: bma-leaf - nos: junos + group: bma + brigade_nos: junos + type: network_device diff --git a/examples/network_diagram.graffle b/examples/network_diagram.graffle new file mode 100644 index 0000000000000000000000000000000000000000..2711ab7a46b091920b996549a197737a84559376 GIT binary patch literal 2241 zcmV;y2tM~8iwFP!000030PR~_Q{%W2eja{>4^QU;j^w*phOLk=z#MK%0_;qs>XfjR zM1x~_Y=yv3{P&gY#P?hnW@?!_@dI|OuUqX__b17;cYl8vdD1-&BSQW6n@|Ru680VH z5`XZ1^Q!w{+uHnnZ)5i#?ekXm)5U=lcqC%d#nt}FaZB3VZZw)f;NeE2-EB)3C&!(x z#C0?p2WOko=8&;qr_q>9CNdHz*`Xr=j2agq4RFXFPq?yeE+D(i-Q?XQ+6(6JyDo9q z-p0G#JN&rUbQrnEC+HD}$G(dnb{pa@ZzMj$103#w-Nx*4Cc^tB4q{y8b)q>HLScx+ z#k<{zh5V8CxP>wu`D75%aUh@btHTiW`yMuS8&gOQ4GmjXRBpav8it|dAiIs3>coUG zqfPhAIAUqnEF9y;tbHFjccM$*<=q8C;-s+Yvrl0H5zX3CGoGHnmIR)+q$dj)TavCC z&(8&O1-|8>s4Ci_9?AMFi5#~x{LH6H-n|K|!yY}HA!imRja((Y%UW~INqaj)lgzBl zu(R{LIozMABW73GMdHB-f2MS_LOisPe~+T`kPJu`2{Tv5p3DzqEJ3~T$ZvXN;8*m+ zs{R<=36R58r1%gcZ%^f66kp7s{D+9kHe$GJiUOdy4V7(ZN>JZ{a7Wdp{{VhetP@IT zSe9?2d*Vv_JPv6@%Q9^e1s;0rILNDX%(IRkk~5OtOX+o4!3W~u?qg69Mm*BX*f_r0 z)ESSk&&ny7`cbJkavGPBV2PTPEe2XKzEgV+MIskNDX%&wkSZ<&X|3{1{R#1LP5R8_ zK2V<>`+Zsv%_+}Nc|@xH2z&RKaqcSVp5`6-hKpixXbDG7W9(fab7pc!7`clL$|Zh$ zFC8PJko~pVi?VZuH$`7iezWM=+&JVYP6t3%p=Rr*2{piZ4sJ=%v}D~<4AX#$rkjdk zKUW;JNM5l`*-%Z>R&@>7y2g34Jcy0FH8pHk>%M4+TskSbFb}F{zWL-=_H#Y6*7qU? zyU8hFQr{COca%t--~nb^5Htwo_(xLzms4^M9O zb;+$lo}9L7*p_Wtnxfk4UFmsn)cIBhyNmFzg+J>c>Hq;HxhBgly!;m%IFl?wAKr>ZM z6>r3#4rE0&6lm)lZz!g@FafdD&*vX4?v} z+b5p4ET&r2%iaRjp%NG>LZRY&r5Cb3>>)oQ@gFA`Kt)%24s|FJO}YV-AJO=h z|5gClceSXlF&zN93rpx)e$!s84MZr`^aH51RERi(?zNx^_)JCJdFAP_BQ_c zW$e@FU)5>n2T+%y>at?$x@AL=Y>M`aP?x!;t~GV7sq0Ns7vR)|e-?4ot+}9P${K*W zsoEM}t3y+Nd&Fg}iEB+sR0|d(B;I?pkx#+vctpuDbFsFDuEP zr9LbcT4hyFg~GDKkhu9b*~NM>(ORlQn#PhMTdHR8J*TdiCa~?SE-8^bMPF!GE1nt% zYAV5obP@@vN}buKgHENqtmC5~>e3bP#Zs@sF>!HpLY+J8mdlR{>6k-blZf0_OPxyH z#BXLV&;Um(lzAPGIIzVh9frdd;Qz%q=+a`rRr=~SdAlo$8l$C!PU697d3n0-&7~qAFyGi z0|g|9Vp~8_Ij@yEjOAZoAe+#Dtb{Kn8#nQ8UiMUz?Gz1s%wB?zc=mLL7_(B~r&)1I zUDBVH?=CEpv?XBe=wL^O^<|QYBK)#C$ur9E>W_0C66~{rgX;@tXJ{TSD=Qbgw9m)r zE?NOAQh9#ToOSjoV{~+i!dwk({(pQk)qRg5R!r&XwFZmno6%L$!x0V1H|isg+fDpZ z&{aEfs4LjKrlb8P`{o{f^gi7{fdAb0J|7>^ruN7FAIF`8VRLwX1$20O^U-q#E&kg* zzk;8Kw@24rlef1H^ln%A`1SM68E78g95g#$Ip)KIH)$O%Hva_o*=yV*7-fQ(@chU3bo7OlCxhG;zR(BFL8;OJIzVCmDi*qZ@Y;`a#(g_P>l)mwS zD%g@##f;zSsLL^kH-dDRba#V*G)Ol{N_Uq?H`0xS0wS%Xl!SCiH%NE)yPm!GIcJ~W z?~U<}d;huPZpV;#c)qpPTyuV6t|vl8Nd^s>5E%*z3QbN{N(~AMZVL(urWx@GctztV zkOK+|mD*ZTQbkTul3c~v!NS_s912P{A~_Y|mAV{$&t@_;HFd2RTw%x}9K%@Alx#9O zj|8?XJv&@*B#FT*W9Ed^+(MFoA+jQmgt4+LI zZtS>-Gul6`o!t|hBeNvUMc7sSGB-|jHUI0=_lK*6_skYQtrU0!?cQ%R^hO4MCzpBOmvb7x((}lolb)Es zA8lN8@}pgKEURcYg`HFC=d0e$5QY`3_(^gj97pp9HUExOxE}P|faUe8c5YZyD3#}_ z`(!XW2-W%2RKG0Kgl9}=w724DH%`q~H;z^FJuN02eYuR%2uKBpOG1XtVjQ2c(vf?2 z(P?1z2BR)f%rfvGr@8k#-|%M zFVJqaqBy9+?yEEnE#fEa5gUZ*SiN7{2C&TAp6ENq59Q!83zLBZQ`1iE2!4eep1z7!-hX}8Jm)inug|y z-AU@xF1Mj_m^^)pIelWRsfN~pwZF8AnS&{Sgn=}LWDqh(jxH4H9~|)diaEo00`Ezq z@kHCoo?7cLI`6wu5+|d1*qyLhrzaI?<;O^DIArrEzkefsUeerOvQCZuNIRFPdh)EA zcjit;lt!MWXOw+xzIBRWDw!iAV##VauIAb=l?cC{Le$%|zY2C*XAjPu^v#b9C>+61 zZnM-%DmBstqBwIOv)A?X={cy+WY|{C^epGz++K$@%ygbcnIDp^pIB_Z6ea$iUchfE zPAHnX4XgZyG>K+?I@)Y@Zk8XGT=;fFrk^NuPeJAxe(Owi9PF|#T#WUSt*z%15`P$| z`vu<2(KlB}#;4tX1Wvad$?=Ik9Lci4H&sfYE<(C9y`cO4hw^#Vw(mfwoK4K6Ccjt|daB&e0%A^)Bjyl&OFTCX`pc{$W za;ru03qm)BQtx_}25k}3VC=6Aml9HBYHACu8Io+Gp)LFiX6a0$9A&ptWK}9Fmec9Y zH>0=5+F5Tc*HDhIn_-sT@vfE}m3i_m#s$!fAPT@S1``EI%aRvjSzsX}nTKSDNQR6b zW$sWkQ(;G-77&aQ_?Ja=xvuJ)A~|tZ1$kyo%lS$XlPAdPQZptZFa&qeZX_bh5y|<; z_Q-0<3KwD^MM<&VYRsw& zpS=yw(+TAUUonftl-0ko3!9azlW~oQ>O0E+8*#(4y`4#kCTnt~#3}G^%3|Hf`XPxicO={zyJ6SrZk{48+y2}) z&NOB_(p+&Z7xn3BUUGiji!!<0R-OvGPr^DrDHnMA!O#yo(B(HkZ z=w4b@454Ss3&1vdH;Ok>F2pp_x!s;U^Ir7c_TD{<-8tVGKNmiSzu7&D+ZMI7Ve?B% zA8`K7VDUXDR=hH(%eYCrsXZ@0-K5+k$mH#cA5~;Rlxpdyzz3cW{$_q_Y-?WKZrvUV zy|gT}zDa1bmWqp+8R^}bESz>6eip^n-5mYAWt>fxN}QJ*!#rO&zFUabI#%^oPTP)} zMCc0Wo-i{qv+Ct*Drv%$4QghuA+G87AoXa(G+8UwT~rO#RMZ^Qn@oqy;!bnTG!r`r ziCEud99#AT)Vb9T)_$&Uc~fG++gH(6@#CPbrAWA^b?hMnc93GkBwwS?KSsZ7e_V97s^NU^J@i{ec6?m(wvI3SF&%g zzg>86{^~yLxU~OZTQDQF*tHW8Cxj|IO*$vsD3qSlmoq2S)Qs9ZZKz?0(Gt_#uqfD) zd!v0-c)NG(wf$z7@j`h0c5!&np>@eAU_i9@d+&E)F=ld}Jj!M9JQHJGV*}%Wc(alD zytVd_<@japc3z!ECiJqDNzp#9u{*gEiH{Vc6i+EIB$*|DwJhyT78b%S7!03i{n@3^liNwkJ)T4OH=SMz7_d;eH6OV~~Lsm!L>x&G7T>70Y&b$W);Ni(ajO$+&`~Bs2s+d` z&n<>>W*DlmpGG!TFH}!iUPeiF3A)5*S@RDz>>SbECU)tcBozt{bAI7C>@5GLgJ4lM zgg4a6)#E%A$*+70dm4})HM}ukmubo7aNK-wEpqymRSd6?$x6?v)^WQdM_N3}ow=B4 zNc+ww$WGk4eU9c>UN>cj#h~_VR9Ref^6B%ZkthxHur=DKwduE6wOQFAdei)C@3{u^Agf<3kFDLB!0*Na zH;1WV?SNKWiJ4z$@6i&`3^wvN28m*LD;BRBf{mP7#=cgYBn=fm&0KW&ZQok`!$>q;PClKE>x=Zg1Xzc3i zHR)MimI~A=sZBLm?&QR_Josepe%}4c%SIS^8RVDfHyoh&aNhk8e{YANk3K=h#s6&^ z^Ik=jYK*U>>U+e}#AMd=yqD+LNiN-bHm|9}d?Ww4#T4_w zXUngg4&NJ-JS>k63VcF*q%SoNQiVsnRL;YGkf9kU8OSu=H?ayw`+HtuZH`9u@W+W> zGWkjPs9n5zwBF~uwfx)?)Iu^`B6R3Q;mtm+byc#Hn`xNwUFa(H(kS%?=f=k{!U(N} z#p}a)@$dbRuXeBruo#QxEkwRXXX3k^6>%(N!~T(vXReOFEXOuaSn-T>TSI+Yc;9n# zQJY51R2xB~YI1Qu4V>bJ8o^%qE{Op3p5de^y`22ywlKOa!1VfFQ8Nqbl zbdL2aZC7nY1pzY$I~J4I4yNWTo_3Dl-B3_Mo&w-UJ9AePa!)&3dlvyuVah+=Apm}c zyv#~T{>NKf-w0D`E2@x7Iyjq?bF*- zRu2yk77tDq2WLxGc7A?-RyGb+4i0AU4rUiGdsh=rW_uT^KTq->=aDjZF>|(dbhUP{ zCx@KZ#MHsfRhW_za-skH_vd|@ds_e3mF!*qdMxmOtdPI3va_(U{^z;Dp+b;X1yrm( z&26=%tnJM0UBER&*g3d)g#I|--~RMpm;BqII{$SjFB{w6kNn$T{yI{K74igsd!j#w z>yKB#c!?kjvHs`Siy({My=no|L2NCh{0e-Bfy@n<)zeZvoF@5U;8y|4fxmbI5f4Nk~yXZ+Z)Uk;n z5|%bk72oauaJ{MJcYERHkcQVQ%6LY4wfq)iqSku6gLH)#${&TTk6aA>eT)kRvg=Hn zs{{+YK!9Pf*d~V0I0w7{4%&HrVf?(PstuTf+~+F2g6<5t&@=iM)@cy+^stk z-F(a z&5PjoVX<=@riXY-&A%8%Hu9;oQt%@9-h!dQ-CD1tdwk#7*ozI{NEtGpoCIrbCLhE? zlp5PxB(^y`2pMGuU0QLD(Rkq!F}f&^9B%AFA=mj7ET6Aezw4BOSTIN$IFMbPwM&?b zs!GICU-T|;UzZ5l+UXUGSw?Alo%aP*R~3q-#>mU8tL2?;T7YS(WaCdpob+DoEv#lv3BWfBxvu}DAL%p6u7BU1 z17ko);W4$Idr(84)VP@T5c~5czpN+=H-3&nb`gEos=leW6^7psb&D% z=u`?FI)0q>`yB_@bd!g}JF;E{4#dDL|KlI{U4~B%LY|%s!x1mCwB8;Fo?py4^jw6Y z5)|BA>=PNCc9Fnf$}D;Bex7avS1P$V-^Ksq#XrmJH2{WMMtZ6}GY7Z( zH3I6|lE+c_;d(g0`{@=r<_Zdob>nuiy2}8&jJtBWo&Zd4z(WHJyTs1uhcH4uN8@dt zX=5H3%%bo27jxenhxj+6hrKAnpri@{?stu<#1W-9hxp6@R`ozMezLIMmt+Qu`Qr9$ z8wc`aLBk@q3M)M@YQs6glK?QYST=VuC%|6&z+(D9@E3a$y|;11osMO;#!8TIk`G*O z(7f6F08EubXtb}0NWc+)NQfK`>=V@ZL#Z%b>c;_h-?#`II&kCFO+WN0fr1Q$MGhks zh=>XO-+vh)Kvc=wMHO{<|3CTTUxz)F&g=0xY}s^svM^;7@R@Cp_hs}WF2%~j)gbRu zm%6t}%yd(aP_@_|74;oy)c{ANU{DVE1Kuh-M}rJGVldwUK{=MsWg2gdu|EdYJhT}m zw`A#^uc?tp@#lIg(>R~rAYJK-dG9MUC?hGPeDunJ=ssZ>Yjml-1v63|Suf6-^msm%Pv-8k7yaIUu+-(#1maT@37FRN>=!&qYn(n>n9~SJ_ilgurti-ih44Y zcUuS?rlRa(&gvguva(h6bVBbH^isctx-LN-lGemTArPPa5HgtYERIW}b04cpX36hz zVH%O=O#vTP=gL{g-8e(DW{g=4twNTV1hI|nt4`{-fqg-}qK{!-7{XEQWnw*c2alVm z+nn*^^lYpm@!z7#vJl8eq@hnDT8Iy<<;i8gv|Q+Bp_v5NgqiYX<%s3^Y$R)$5={%+ z|4z4z{Q`-puMMf40(KG8C0Gefv;Z9o3}f>|`mDw9u0EfZCKP0V41A0*DW*!EOPnB6 ze8A7;`N*ex)P#x|Z4OW(g|_f@&O7n2=4-P5w?MkOR#pnaSpI?K|G@J9-|#{UdrUZF zBMZ;_+oi{$a`lpdfQQpuo1KYICaIQLPA|A!w?+iqzW?&e7V@gj040tu)Ud^yA)b=f z3r;M4(f@XT`Y2EQ?W?6$zqL0x-z1@67BU@L35e1xbG%+XKHP&Bdnp3LxQ%>|rL8KJ zb46@`u#v=RX#7bgjubgJ;VL%6|7IJ-X6j4Y`b6$<Yi%^zskII^xX z%i$c3>@(wcFb3#bC9pJo}wC zf^}%1;FP}0I~YoT*&(6R0gceQqE^+sH%PQ}$+qwUce%cI$35(HU6S=*T5n$ZoeuKm zt85k~k&IZ~GzBhoIx8ug4Ii2WOrQF(hWX8^CG)E^ep%R`#q;rfu|fgEN5Qu|?=no{ z(s6L+7zAU`El8sL-q3rS=DY)i^&=>QpGonLwm z8kdVcUov>?GE_hKWJ44U<$qT+bhQxG44W$YLCGxUS@blkTlC)jl&oc_GbnPqSD~1) zC<5O&Mq$^`P8o#Gxd4gG^RM6Unb$-3KsbM8{yH4_nL;e6f;&rpqCjjDX7bl8^`k8s zU4$+zLV*}v(AA@D+VmW<>*E_~hVuGAF$AS=Ljt=WpK9q>{W5W(AGx1u()RHOmGcu& z_%s(BJJFC#Dm)GsS8VC%>qb$OPj+)E{RMS(ktibrP-zL^Fx8P*sfB_z2Pg>U$2E_F z>>pUydbB(;XfjD1k}D$o+?s<9ztOi6Ek0^sQyg7~BbWnh=5^OmcOB4p)PYj=`Rk&t zU3DL=O~Xb?*(3+hz#bkjPT&N(B3f<_nkFZ;j6T~p?bmljv-ny{>0`<;)tC&_Ufe%p z)``@G!iT3bcaDRq@`!WdL|GRbj87Qm4ZzTff$jT1@n$q7phJ|Ftc%~2KC@;&hBVwVAmr3hL)@otg(v+<)x+{ZFc6M|nr7R4E|k4))U3Hu#NWdG6sy5Fc2tY}gH ztBJyypRa)`wHavvL&C%KZL1(^V#YF;q;;x*bV%^9*`_F2Qw^fY4#2k~Rb>?mc1cEe zGx$Y6^iVb3k;cY5283YRuxwIY9I(eR74lD(bSFzSLcmdlx$DL0lrN?VMJ*H1{ zLL}1keD>=mhy5>V*nY(f%)R=B+>O3LfF$~JKwH0Bcw6XAS1X|i0>9s3%jN2jF`epA zNcf7QH;nVmLDOM3&ynO**5EVzqb`1!t0266<0lEY5>m-HtL}?#QwBRJdiF@EWR#@O zBwmp-*H4@7eEblr_CY4D0-oZ7*ydq&3e8B%pQN< ziJPC=({^QkDZQ9b=67NoaY z5_Dog_wtSjP3k?kYLI!#4BTgms4SdSs822fWcTj>MVzhUKvK&g(K4T&-(F8r)=V{U z*Ac?OlqZBl-jUBj@&dp z#{wy~|6zgbI!1u$AqLRc1Ex!u)$`=3C_VOr14Paz)G6qi4;puIQ@SW)2smkduRUbg zUjc;`x0!U;IX_Alu%G911B|vyS@Ck(ERM}rIyqHIJ&$ph#CR!|VZAPpE)ds41AA&P~3&{!<)w86HhZlP$q~f7cQ!aV=J`<2P0ha{I&adTnV0P^5>Yz_Pa!_?>3fkrujz7F?I> zxf-zos~6C+OWY5_?K%U5Th84$*@bmK?$%g%uR+s#Zd&xa2rXsNdsjYk+t3IdH_?aV zxQ!>%#YM5WTlOvInswK|Gkcsi9U+EF*~9(qS0poGhYzyr!wy}G|LSkJ$i<)RHPUw~ z8R|K-xDiu(&X}j7rLt(c>A=DZ;2#G6nvI4NaO_Z5;+I|_#IU|x@K{pGE9+Fh`1L|} zUl($RVgGiAzjJx#9VM@p@xC`D@XWl%aRZb|Q{=r)50Yz|Fv8}1z~g=Xhe{U)_Xll@ zbn}i~r5nkF-rTPdE&pmo{LUcXm~mGPHJAywom(m;^S`37_3sElvH_&b*`&1JPAzYj zBgBpuN*ukZWu@f=)Z0&SA~&+kEoWoM!3zNA&Dy8(&Qgda^rC;^c!P7xFTVpJ zHMNdmq(O@JyWJG1=G;dk`)A|I7=po`HPiaGooKuRhiRK*u{c|#zDMG9pjdWw-B)&^ zqfAlzT)f3U#f@lgBPFD{_*Z8l<4y2;%Qye0OU2 zT?0#KTz;})GpEe|ILgTNlfj~G7?d=nVDry%#l3#1 z@e1Y>Wx;0sU%*YmUbw5LxuPR&``qw5V+SloM8TTlZlW+s10YtM3T{@_u7OHg(mrG_ z>oUY&;(gLbH#w3oDcsnWYaJ;C<{xIVQXzPK7z1L}0)N$2_#8|zOW;2Isi-)k~AA9Yj( zH-gOfv7e=B0K57?JsVuh2>ag_kzyqI#(Dl;wX(>y)F1HfWWCR1oZONw+f{AGqH&%S zzL7<8+8uUS^s#x?xu03m37vC`e2dhn3(M|n57C@tAf>9$w`3>$DpuGZ7qjG#wivwl z9;$gRK{jvfULFsF%A)kScI@TO?csL}LHA!emY=`~wA3kif1Ff>Spblz_(k-y{K^HE zv5izC(kDq5fj%?}YSbs2Pavsooa67_o*Y(Tv&c94Ijavb9cYvzJmE8IaiRwu3aPV& zVpB9%jjo)0%M@Y6*!9q3v7TL2GO>GVnawk#(2-IXX~}K~Rjep-x9+{7`|K`m2=D@0 zy?9?4e~2DGg5ntU37|^hy$K+uePL<6T3to=E*({cA#p!>ChYBsdJag;Cm{Ec8hOkj zNmB-p(*1jM;)?&=eH&Fx6(^*2|8V&oBQwYCmrm!5t40*RM9|Q?O}#g#aRGN`#3yiv zQ2?ixF1Gl5b?1DO910~}FqT(qA3m)osX2?bo?9e7vk^)^;zrO>PZYnNtIhBARbZL{>QRp&5#;H@eT^#Ec(F6t&)uDDmkUzm)Soii-&3VxBADe5 zd(|kc%_{q~<4Ptq;gY;|v$a3%aK(cvY`=V6Tm(5e?`Rnhi9G_6BY!zq431&u$-W|d z{|gPv=S}a+rl7kTF8Zc<%)bI6@y=!bo6`;`3uw0jo=3}k>pFAU9-d}0NUkM0-lel3 z3?=xE-=Bezb3(K`&nyNBQs&CR?&+KMGTv0E0>~si&m`heg(a4Z4ZPUJX%HS0 z07K!q8~4Uj?6f10olT{q*FzS?DI}f!w?#qyD0H*+VG>ktLUjA(wjkvCvJ8Qjd4k}1 zCxB2&bW;H6<_c{0I6f3ELkSP$vKx>2OZQ5 zi>Pbw=oSG9D3|wQ(Z|7U_w(0Kl04wUV&F!AB?)(@L;H9JC>jTRBNwIfRN~z@Ex4K6#d-OoNoTuEH=PS70Ejhr3m}!U;ISofyC`$`9KZ zx3$1*x0YJ3pt+`l7eM*`utxT%qU|0WqGQM6p|OG5SZGF%O>-RIOnH%L#rst{c!HQ{ ziUxN}pK9LwHWBXAxuIJNEqr5z*>tvEo|qQeA!IQl{3~u@?h|Dg(YZ>7$%faa(JGRC z#gT_Mh~-4R?!rv?7-o*>&=W04zU$axju~|9$Y2 zV6xvCfl~%*IBP^o(ZJe_5?6hN>NnBArVd-i<5CQ&fFf@@l}FYbV3?QGA0}1c(Yswo*Hh90@%NCYySiyH+}=f=~$pDKH=_W?tazF5@wFIp2Gs?gF@O2X;C0OT;9`c6r%=?|OmgvXVbVcL*n5Pt z1z;9RRR15(0wB9c?r3(Zd1ayFAf8WQt1iQYuS-Pt#zFT(5m0AS)^ov--MtJNf+zevmyTC-1s0XrG>qqIxcA`DZQhFm;xu-*t4d?QRCToefAVzQ0BLE2+F)_Yc2Iv znMK(@QJ$JF*C0M>IscvM&^;LjYzd1@yZWm}B|4xQc**~X@IW&v1mVqE1HgFX_NhUs z^WnIMOl!{Ws{u?65Rl7qAI?f3&)|-fC?&e&wMp`a0|Zn_<(BhbdN#AQ);quodxcpK zT0lx}E)Tzif*ujENIad_San_4@*hb7v2A#x=FTwUHltC*O)eMza@Q3F4LDWMh3FL$ z0D%r&QebPCtv~pzz}}Xgh+8{MpC~if|9Xx-W$5#{5oqbFI|yXHm4w_juY3(kZ~%#D zruh@rgKka;tl#GX`1GoALchf1u?-djq6BjW&CsOkkCD&PJV(@@#-WlT@up-}m~RF7vr@r_`qGbG_P|@~Rhe%53bC%400>jTWTI!>QD=i620# zaD@B4Ed-Gc`-V$RK5KY1{A-wrKA$LC+y6pF7ri@rI}1#gNuaXeXg%&4J(fcF{{L~b zTT4UAHG(H-EcND5*$?XH?A14O16sX7TLRO;tCg{D*&d5J_v>_`s`9KYwUaF(dtfy< zCyf70gzMWDQ;PxWEObXd91QLjwxeg$5e2pvk?jxE+dpG~`}2`9*?jp^Vp^rFkDA&h z@7VTUzRHC$2!V^^L6rRi_vYmPiF*YV0gP^C;hwUd)=G}AlZbCzw~ETd8eW44W@%oJ<6#+pjxWj(O&=MxVf%v5W z2eO(PhH#*(fiT%uF$uSa?=betej*gLsCJ3^In_`SAKh*obz*2iY~Oj?uXd!-xew=e>UYy3?o*+U-avog1^%(<-webD11 zh8z&;3iWsDN=B*5{o>#|zDVVl=Vb3MUSK_*{TMuPyaKmA36HOzrzEgl=;`v|BQW_8 zkx1<@fuaIPijd@=B*i@gFi&3t8;~Kw;$1IhEa}+h0NT z6*>JY1Bmb4U*{1#p7jm30$ak-oG8|l;{8vc>3uQuT!B9a`f3?S1uFSVI=~Lbdp^J@ zAqGifj3=S0N#(;vTjLEIvp@=Ro!Awy27J`q~nBK{2N% zN|==%*>1m(%*NBt#fmlc?6^4@MJ~;Gcc35>_$UL3UfsV%Z}fjKc;2;Kk%pPFFRi5@ zF^azPJ&R&(RZ%Zd(qL`9GWBGVh=eh~l3*JC^Tzs5#FB)NT@?eddW&VLp`3AWx>}Lg zI%WnD6JPVP0K24azkZHZp`!I>X9w6dsB42X$j&Smvv0nH=Cypk9(0(9pvy_~N@Z*By7wy%V4gh&^=R=%`ny1aXnOC0Ydo$|i+v9Y;EAnQs{J&Lz zZd_S;7N9{9>ic+q8F5<~)aE*_nA+P}p_)Xih3ZmnRCha^qt5X_W;cYFiTInqd5C9MePUD*ex_y_*2O5wq!)rz*`jcJtAmQ zR6rA&0B=N=RmO1>s;!uPFu2$cwItrp#N z{TV+aH4*VmtlQjm|s58k##wZfusURS@l2!hFu2wif4EE2W9FA^AoF^uKFWx@)+ z{62(sqBb*3WHX?V=8NI~Al~H?NjsA`Evx1;!FvqRAj@JS+)q*#`Q(3IW+a`e7`Hs< z8J7{kxuwA6x%eC=@vcS*CVPx*$S`TKntw#JvU3Vxr|nwSl5BU~yd}03c~J^yn=u*4l)iSaYqDw zq*-DCa`hp75R5Q#0lE$Gw{(o#ArkN&@Tr4x?0%`x*%=MLK&eH5w3AZHeU?ckN1l#SGRJ$IV^4>hMdhC@+@qHo9D2>|2X9bQ{&cAP!%xK1|EZtDci~%{yIMx`EZ))`s+)XPALs@LOUEW4S#MNYwRj>i1JI^xcP$~>wC{@`7DQ6d z@7AbWMX7uwpH}(ZiLeZB7MgWrUxB(`;;)rSKzG@!95j}KLxsCXE=?vfB3iVj4R)wUSWgJe9xnVugq@2G? zZJbj1TT4TZGfDUBThL2L`sAM1Cn5&3;`@Vl-PlXU^qGiLkX~Qef%%CL5_Vz1l7u-P zuk3@04KE2z^9a`n)yA2&AgMnuZ|b6jvvLGo2&ilQ+jL(dE(TE)MUr!zEG-xmv2mO> z20EP9x<8r43G)m|D4;WkGepB89nbvIee;u5NOD-=)s@0%a9*%t*D+_3 zp;9^KTk@B>)M}uy#?Y0({&~AXBr#o$Nl3SHx6NX(J^KnQa z)3!RI5%dcTGC8;pDzQEJBRls!BTu9(Jb4#V7B&Ym-(oK zJYS7cz`+k?eka3PrZHdBj8^=bjzaA`>+^cDB^>BMak`n!b z6Op|WgMH`3wAp$L=NEhK6SH}5^=L5^PxfvdWwxpTDTnBUHhfOP;}C*J&cVb*AcwLBfVKN(9UUTv7pIuj&C68lVQa>zXM6CLwTH|M?NgqaO%#5S>k^% zJf|lhf`4suZnYte*E|?FOC({ukk_XYnJrPyjoo-V2;$f2&-nd?iolusXRUe)M}2LX zZ85_B#6W9NgZn$6@ni@)GdgH63kfo$GtQQ0OrMYZ)u)4cQ|aQ14vP_wHWMlb7W56RFyB=?sw=++Hb2Nzq{e4 z^Pujkkms3D!d!uA&qdj6kN*c-8|skH693af8A(R9iW;=58RS8O$z~v|8$*@iP8t!n z;yEn@LE zrw938S_h%85?bKCxHQI}R#z70E?peMdPeSVa1+628dW%sr)P^b7ilYUIq$Mo-9eue8VN zTOHV*ZoLY=6@U_O4v7hoTs4+0bodHS>4E#S;@0#R8Z<2U%Q5=%Y42C@zguB$L1-Y} z;lp>EX(#!f_wvn^yMnP1Gt-euCH*_j|zS=4O(#N4Xv>x3}A zxi79OJ!ApITWQWnCsEltA6_H73UnLt%?%La@ce935Sj}K?7$Qqtgrk8C&d7GcZsOt zgt(tx`-CmV=JxT9w%7LaAl4=e8TyfQt1DrMa6wh{H{n9SAP<&~s{v-{kFd^wA-N)# z_%vO7(19?%*6mRE4o{bUC6j}H<)i9{V91tkylrxK&7jO8r`|?j3BE%^p!GIa-vVqfrI=hXg$&qQn09 zmA~Y6NEO`nu&o@RCNQMKhkGyj%$__?l<)l_x5l=C1WMS)cSUURUvyM^&{c2ba?_vY z8u?#uyi97lBBZN)tBB@Wo+Ra8hA(iS|84ZGj!~3RgyOS;Y6u(rTr(svYVJ89uaWHU zoYfn}!ln93;J2PD0jbBd34+_A*Cx$LB+FUqyRm+!WW1Tx(n_Q`*|rBr*Ud0wS4+`a z05wnhh_dozMbiOc%G?71=)N1P`Mo-KlzzHX_T{;)4OWpN@4_qV0vv--8C#y9<&k~b z>Gp30A&OF!5%1DLIlK*2q#PLiRQBvYJ`2Eqd!rvB>{&d`L7@mFZ@K9aw^Hm;K;@Ok z;@F>v_K(l62mQg*p%3j(?fr2qUoF2LE0jCFWP9*9=cqwGE9Gpg1UeyuusQ=VQ}Ft( zL8YMC@>3!jo`?~Q-8I7%L6ls{?8G*tDwFr=ZP5BoAX5e34ugijmCXV}W=wmq`-x(1 zg5=8BdqAKzVk}sPvPnS+vDF38J4Q#cLeO<259#?crl#-&2KAnkVTK2C83bX@{9qleffT3|Jtsw2pRQ|i=5#-jGK-uCfQ-s|GarM9_%L_Ix??y ztUAD4o+IMQVkQ(TWfMNI%>fl|0>HaYRvTaIuafVpm)lsrAP_r>6(Ba{?F^z|d-337 z){Yo)n$XPv>Eq`B@^H19$-ZBabOSWu<$9fS6+}kyg_c5U z>N5nzeTBvsYA?YUql3hsiF_ph2F|W49BZX+KNr$7eSb6cU1|Q0-XTa^@6eoiszFD# z(;BK@0q?HWbi^dnuwakpQ9SJ7C;A{OsDMA15VT(xdb=u&&VC`8`RKPY$F8>-TYhl{ z(N%9bmiNJ2(;E1D{XIzQPh z9g8t|tHg;Ld&ncoKqmK?TxHl$9+luw4nJ#T{q5DlyI~*UOvXFE5QHAi0XE43vd{Xj zothhu+t2up%^m=Vi>>MZ9n$sy+Cb4gcHUw6@~*TWv|KI{&2>e<(J4H$U4g+^7{|D8 z&v(1Z#yhAmd@9t{(}hrY&=ph~=4uv4eO-d?RM;IyM&=IVJr1zWuj%~A^|LfSr`1RQ zK8-^4ibiNkcc4vzez}_0vIT zlH{=~*d6fl(1K8-PGHC2BW})EhFnjAx>TuhvEl7W=AO(Hi~o-)M#VgN5DaWO%I#ADuR)Z913s?@_Y=F2fGv~u0J zSp*|)ma#iqE9wB8$v_sj=X!!nDWEvLPF;|q z^Av|8rV1p(!Zaxe2}8&#BFJy2B2C-M9+f0N$}TgLz`~^@j}7Faqb6he{niu56`QWs zZ(J0phdZEk-)@!X$h-xPFQEs8QiTTYNRK`vFKFr4_Rwd?lf0n(*~{ z63f6uu_6;QPnCm*yQbX}{rp%MpMjkY?4JN$YHyP_q19|xH%1j9o(_wCmuMyq=kqk_ zHmI3)Q6);F6t$oKw#6Z2=ssg^5a|~BqLK;LXu_g{Bj1%tuEi^)V~?HFl!h}bh{vx zJ^CJ&#|Q#lEeS@7c_v1tlcN(HoM$p*Gg=?Ln}si2AXM$;Fw*QXI;jxm*3L9zb%o}ofvvcLdQO4ZoWSI)z{ zUWFOViqdLJ*rl*6R2O5oE6BI@&$!5HDhdV#Pa30jTrqQx-hG>~$lUPH*LV8y6b)tu z%+{APbPX?BIs*)U1}z<}!-8fj|f=?aQCU|b9T7N9ynRU1YFgE(MI+x63yccfxb zSd+PKLe0X(#5d^=S!@$b3zD(@6RRJoG^u16h<({qFU)XJ@F*E9cHF~4!r&pclo0OS z7phi`Wb7MUKMYtOp+J*Fai~?hGcVM6ifA@4Gb5;12Qg~t#~vSecAdK}DJ{OO2+lnP zh?ZQ$1@k3*%^v$4X>+xKQ^A4nh;LjQ#xk|4uS!jMxy#+}cVb~_{Qw()9n>xON>+lj zLU%76@`4bE2UlOt)w3FqWlBl4zR)JJ$EMf>s5e4I?utyZ zI1@4-zqLxq@iODjncq=Syab%C65X~4w)*$tJQ@nch;)$GaZm`WCD?~EHoevlm6-xM zX;D^^)DfA<%df`T*p(|g)wH@&=iKM)p60Zf($ZzCGiHt(>=>;~0v zs=;(D_^h4(sjmQIP(}80J%kpFl|Z?!wq4z1BlXB+Hb!wvU|9BV>~Gcl6nvPZ(b5>^ zq-BK$UCtGzsRvVAy+0b8R`Z)&f=3yA=kinrat8FO=yx3 z0#Dlc2sG8dW4(X>5#8W`$FKH>VU%Ieg9IwN&{3NhYZB@iQ~Xs6aZAZLdPz_YR!T%d z&gWk$MywQr<#RU0hS>O4;w+;FVCP?!*DcuDpZhUPT)uyGn9LV{E9`ahfx}aQsizQ4^N9Eku-@yk~Q)cMzH*5p* zrH6FdbbM{z2MUQ_sJqGAr~Cz7=)#@T^{=)B8Qm5tt_sl{uP=q`y9qngL&a|wjZwYv;oP8ZU*|o_gNLzix#W^_(fH-QhC1By+G>`kkpfc5Fny28D^Viq07O_5T zM3!8o^6h6j^FGUF>dKBk@&J&vyS5A;En((%E-JY*&MoTphamZ`pnf?SqQc#X+jT8; zeRq6$zZ?K(+vt5vlJ5^y*C!eHUKEDi6x&9lSI+)3_Nu{*XMd3ygEL51pf;e)z#N$% zT=le^?+80v`Lu>-sT!R|-utvqjDXj=^u}$vV40Zo#86O;AOMio`%6$rSu!=<}1r&fK%#b|Y_2 zn>2!*b4((33xYgn==l4|UQ&r?+cymm)-uaCh;$87dZp*`)wOlif*zH-A8VpnzWd2X z_|n4v-Ispq{>%G%C6fDYVhZGa>bohfJhlPT{$ceVzJp~l4<*dm?m z724=F9|kM)42f<;V9z~=?hqG!@X2&^j~MMEw&oW_(U#}p*>1`?TwK~M$VY*KoY+9? zaR8&2mSt7Ow@PdE%oi6-22v@vONg*7qbkdxYdPoarS;~~X2;c#^O-R07H`<=sXrSv z&L;=M#}gQM{nPN#p+w{owCW0U9Qkrcmij#hXi050^-_J{^wnF#0L#hYBhFQu9+IW! zcBO1r23!!e*jBa9Ur(3wU!N`@t!{3J^7_0$m!Otmn1tx;D)f4tZ;eQKsRAgN-&%`S zUh|Pz*;QR{Y&hL3%rc#ON_1l)1`}ig<^O>uQVvABPdFXCU zRb$%|-Yq6(O$?VD`*Q1X%S-DK+Q<8JMOLa!Y8tG#xbi}MJDf6EwwZQ%L)D-?SVgl7r#&_L1`GSc>+D6@_xQ%emlJM9 zCZ444Di3Y>dMKTw-`E{amx&kxJy5~!fW+Q<#pgElCFbWVp3fYV0zP(08bQRh9-oNDfwuqkHhGzHd#3XIBC zU7OEk7>s8p3O)1N#H6BBSibBb+kT?#diCW2_1nC<{R!DXV%++V@D`*hX!bqS+zP5W z_j!MPlD~c-;0l^^gQ@4un=d!@k9+O?(>><15Pq9j<1d_3|C~2lE);GAUwLRgqa!Xv z1S{kHDcmQ763dsDZuprndY-vOSnrhP17|(HgDe{J(Dlo2X5P?p8>%7Lh-Nb0&rJN> zln=!pB0rc2gb^@(;ODR>Cue?3m?Ug+It~DSUx|X{lk;`U%K#swAbq0v?Ru;4l&Qfu zn{Mo(emd;{zR~=_x;&tcj0_&WjDNo2_P+{Upo9dW3kH2%inl8mDyT5@NvM2Bf51t1 z!9#jQLnS5jY}k3U&>2IK4Bv0S0?=4yelFWvjw_LtMYZ#ksDy=PkB$lD92oBYDwhnT z>Raao4$l^2qY3y3Mo5eA(LGoP0nzL^U2(S58$0UEp7>usgz%lDOCZhm0#}n=-J6G~ zj%EUZE3=eBl)NPITf#<6FI`=YCkbg@TDagaP|>dtD+pdZoXW#PS%bqToynP$wr6KS zIq~?~M!&*LWUxe*s-BxeX+Y*8Lenx^A^MJn%&rNhuZvPCIJI^51&RFo9juu_1+H9Oq z-@?1y1V|qZTg_Sts!+Es@3vG6rsgc1M?>^Vey9ZyMgss;Wa#M16{}aO#L<=IuO9x~yX9T*Twcq9yXmxF zispi`{A_miOj)UZ_G8s&{SU0WN+@sJ8dlyU;#uDg|Jiag=N)FX_4qPS5Hbq@owZ(O zd-5q(&N!{QUx~QgdSPrpj+5%X(P#70ZcWWs4=OI(_d6qg3=W89Pod)bO=3B>9iqfr z2C;XKQ(M(ApL2EgG3i(B05y%UXKwILPV3-CeA(Hgr`(Fm(=Q_NxAJx~$+~kE7UU*D zYNhS;l1%n)=Gdx#o1a|FfR zvPrE5oJAm<;ItR7yBFFR4La4X%l1l~=~Wtr{F@z@!5Fou3k!*dcbDz8=S+7vR-QOq z4a07RIj_cI+}JAf1^r`E>?i6?uNWho>6l2ON=07;%=OQ9QrgH6(ildnsN%GisJdDb zm1May^vzOmhg#PEe3Id`i5qPy`chO7>%>q?--Kz+pel2QR&<%UHs!#FF)^Ph2oIxA zo-l$dI4?bkq1|RKbvxp|fQ@C7A z!7a~bjfIaijZlyp-;4MkcocmO zc+R!#yP#%d)1-P}JcK7>NjBbfN3;WBDH5yfLm9m_YfoRwbSVh6*sJuXyiL08{agJM zM}HBsIvjLG9C**LISJbck7;Z^Z-G;ZCV2!Jw{+^I$6@+uXW|WsN|x>_CkJl#`9mWl z9CZ6!KRy~A+K*6VXy?$^F~qa-jm4G|^wvK|QTgQx0XcPH(kpJetnv*6N0nGH`SK!AtkBao*bn zEy27{pqJvOvBPs<1txL+DUF4d|CGi9z;vE2bg&y3i}zte3r0(Lz#!dUgF2P=yidLJ zgp3jALFwJfj(3O!%f=um3{4Nh(Gv1#_rIIYA{_qO9iYY*oWjy(YL3&)sIs)8gFBD8?s2~zU25=GpGbZ`Y$YfO38sUcz5B(Jx@-wP!6Xx z`tji&xD@kOI!3Er1DRU{7yzF<+D3to@>u-HpJ^a`8v>6%a>L#~a>JbW(hqHam`%UE zxz9uw_##b2%9F#D^WYYk+jzQ`-;?orGmXv<=E757CUi8xKH`D`kPi2}Kc0h|{Mbq} z?e}CA%y)&4|L8aWtGi2uS-n*E1zgd3U?$E7H`kG13Os@Byv>|UB7ra>DC2p5gn)@A z9ejrCOkV%tmHl6Ib3yN2HvyCc;usxZ?3hA3d4Jwv0uv`(I0UgVcAF?+K*>%zy4>^Z zVXWS>f_UbA4uOW;lC|;DcVL2;EC5Z|(BnY%%!LFe-C^))9LIOa;@$3Dxs`~B?f;My z-NAVo5in+ejI>5?bs@djW4&y22nbLKAIx```c3r;FSNEh01hPa5a{so3{_WTyWjDgPf; z^Z#F}=HZJ+fTn0799>pyFY9z5?kPg6kkp!$t5%kSu?%Rp3dr@%&yRh&UYG}Z85%t& zf6%lHe@{iIbjV;Qzb)u8ar|2WhnzU0D2=&Q*0=I;H)Y&_&Z)2VB+i7X>;oF!!xSx` z=)pg;=8>SUaPRnP3EuJhF8N0bNeLf#cDDFCk;LDT_($Cg!2R#!u*w4RX3vU+m6t;9 zYt-Pnnj$g^`NRQM24n$gu!qvmhLTXSkm0-g$hrhc%sg_^Vgo~)3Y^W8cXurgJIHL8M58DAd zQX=Qr^-l@&>kmRVEC6Vv*q8jOYW-heHH1NIsH_NI7 z^nvsKITxV;f-uTIrW;)MRzUTjW|m(TrFOoPYa^ca+v{2Ax>X59zw4rawU;&oJ|U?H zK2$qtsx0YQAW#sFD2g)wg_r}LQs2d{Pa(aZGBhG`Q=W+VO#(28O`(Yo+;N`6@?_oW z-RR-b+Pi{Tts2JKkoYGF3;#*NW%r??5&JQtolw+(Sg#>`D9(zO2c2>pRpP+lAG{N+ zvo1mk?0Rn6k04xbkQfc{4}@?_%MQ#Jw#qO91gO2o@;A&karL`%?{p7jFh3deH`mI~)Xm zQ}=xHO#@YClcrnbR7r}A!ymY899ucJKOcP^PuQ)3rm3!R3Rqkr_G@9Qm zoeu;Y*TyQ*4>)4D^hFOI@%79H5M$)D2-jSP2)Fsi2(NxA&PY&0)IEIm+4H9b?+0XY zQGQ`XHKWk1BO;~mTh(`tvx!iqev62#L{-~HVz2d*o{tJZwr&mpykrB(3LX&I?+3*6 z_Xng?{ox_LI zp#5H!%!yqYh^Fw1PeWRiV(vqUuUd{z-7#8dhgnT@OzpXf9iTL>qfnI^6EwEYrdsAiK6GJDE^(xDVe6-5AhKr#bu==zE)=^?)8{Z8;KFPzwp2t@*hkPM zj?QhamK+a5)mxHXH(H!+UKcmo^O>$dj_NE50MwO5{uMCNdR{qTFxL`5_p8ZU zAN$$K;U3=4-=ZMfE}+7QA>Lj6`4&_bRSQUO@nIZMHu&CZk}gN1a(SXvV7d(cKJ1P2 z>EVA48_~ZGn@>o>$k(=0X*=cg_!&3>qcwvr+L+k@2A@1t;`+6sp*e8JR3s4GdeP*d z>-8G;6}6ipM|sROzsnBgIBw6EQv5~ZY3&ZougR*jRs(}R8yWJn;81dkw3zYEE_1el zci9hL%BO;Wd*TZ7y&6EnV@p=maTwmh?6&}GFZOCihPQ-2Xm3g`L)K6Q^1F<^M($O} z+m&+vkhgl#Og5O(+R{(}nfjs2zB^?{h6>kZmVchm}J(mNFTz;f? zPRz!A=|!X?2g%Ri?8BqMuVC#9GtLxJJca4xHn&BY#2;XEFt9F@K=_!ePvK6}|1N9t zkiJu<;@`G681n^q@wj@M4Zqx}s;4Xt)s5E59+CV?`tr*NxH!d4RKMDhi9@n+8skL3 z>5uJT*JYC;AoyU#IctoAv*D%Yqg`{s>+y#sHpxj7Sn!`BU>ah@@<{pH^#?jeFaMFg8p{y;*}ZHrx(@S@>A* z%blv4bY!Fy@avM$^-2&Q3GY9~PxsJPE%2DG5(8g*r-Pp%n`chas(z2?k4}#^zy6-l zhmydIUggdxzSCe$bNes!1K~a|6>E@T-K3h^bpKJ)V$6CFM8+RC(tBOnPMp1uH$SmF z7qhykfb1JuU5bWSl6+#M13pJ2f6o&tlV~$Zw?2Y2bfF#2K7t5zc6sKR9|Fg6-WuCo zSki9|pXv+jWA?<(Bji<&gJzn!UfdH!lx3)e`ikeX-E;mGEtlsQecMa`D+ zn0?gST(zjm;aMVlccJqPAt#3{0H37-AR$ieoJwOFT{-v~qIOB!8byB}XU_Kv4t*5k zqY;Y47<6>`aj9c`9)05nU8#u3h~!BeX$B0P_}*uG1ToLWe!0sw`a-DX*9$i`2jJ-| zh}9fQq0}wwH;acd(PL7j{!0}2Ig$e(sOYcZ@!M;sx}+pmqTDK!xv6tGzZPt5jH zpZ&-jvtMWS3%@7XjHc#(Wdsi>L5zBmSF33zzF9gXqMSf?oJ-|6hB`Uf^awdC&T`S3 zwYKOz=`oR<&K`pgmOJ*wIqFf|ULVf_>+VYLUqX#+)nWufH_sOmMH>04)Aa`R**u#+ zFMzCu;s7bnP48edotc`<_-GXshLwdP|K?oQ{g>nAFo9xv*#*^cqWbnkmoQq#i|N_#4(uFff)cUw^$q__N2PzA;`3u zb3QP=yu{_BLnsat&cz~ojAAVjCjCiy+9wD|(?m2`YY)F9r`VvqO@+3LZbUa1gQvb2 z4K!V>UwnQ!qW+#*MjeFTcTTUc_R+a-vgYOyaadrwViS8=GrmmVf7+M4MR!eH!20f1 z3kaBn&r6|gSY(Rf-xi;hdZx6(I@)qW+nSokbCU}CFjIG`X9U;SAzAnpy*y44VMepd zy!h7;CH>D3#UnCW<4{bZ%~qvY7+jC%mKzQuZVGz9U{Dhcvr}kqLoeV+4K)5Dy;*o% zGf!lQJ0C&2LU40db}BG~3=zYWPNJ~|-i9eHI{ORWDHx`#V!{z=ROv&T>-lv21JX7u zX*~T-L_p=iVQvP61i^Mx^E6E2FE~2%&1%Olor~I7s55&B0ZyMSB-^lpVe*}5EE={QZWZd*~!VIu*={}(sr#SUk! zUN9E9q`2;`_!i+i8?{*24CPXxvDbeSNIqW>@U7j|QLl+lG;a-RK+hR%Hf`Nl-8tFB z*wv^9cvUTB4{zmDM9e2SXwHEP*owta9jh9|nl6iRNM_Dh2w2miXld1#znvZT=Ca*W z{^)m$*0t*C8LzY4c`TkWOR=tv&^d+|6&j~v9xlr+th@4QCP(Kpt%&7`JtwGJ3S?}ax+9dY?88LfMQ3BV?%%6+eV%WO%e3aTgw#kjF8u+C++%%CCqot4$>e44 zlCrR9GnpEvU@c18oF>@Jse6_dBK`z^uWJ%U{Fo*k3 zX~g@N04J9f%E~TTJ~frKy$2s-1YguXQq{UHvq??1bj$90-I&ieC?+hvja1@G3nVgS z8n-M{way9j^u3R5|I`Y+M4wXRxq@QYq)116 zd%$f1iW4G>M^h3Cw*6Rq?_ahBrs-0LSyq#cB7NjPwJD<~eH>1?HOS4Z*d@FrdH{{F z>=v^%i#hEWTVyM5e|@m$l{~0>i%*_Llz^)!n=|00pjQ;w?s}qoO&F{ze#Vqf0M8G6 zICIqOh5jl@a-bw>&NH-AX`#L&tG^I)?G#akv$hZzG@ie?QxA}e(1$_UNgt|5bhlzH zmTz4>+j4~m)zK88TuL2v=Mh=rJueY|ov+{t*!~tT^|Jj_PzuYKs18sHFqo&E$}lIq zQ7JQ-0Sz|sqigv;O(nGDH`V>z%eQwcO{4@#6_a-`p8IJ4Vy-`<0XBWceU5>C2oV38 zWT|?=-?54^Z$u+`FkTQt0IQJHdCZ2aDdr;;0n86GE#9>9UPT{s;L}_InNf^{|1gt@ zXRsiTX!l7NTEerSc#YpS5^*kcSK+&3v50D@kOcVnZA{CP7E2|`4|wg=T5Mh=f@o48Tq3($0sFHva{@AqF~NndG11-Kx_v^%A3-k}iEn zHhOci0Y;&sN0}o2T-TQ`L+`?5Y6WWZo6YRS?AoZ!6o+-2Bp`O{9p}W63EA$P7+`W&I^!eY~lv*;w{XGX%V#h>6^+TvN0kSQEf1a9^-``BAg#_K>;FV+*4HP+<&db*@CbDUjB zS>QMyPxtc4#>(dgkB+oG@6VO2E>F(;?hx1dmx)kJH2&BEb;bG=(}iOiAQ~wv%qzN< zn?5D~H(Cf1qvvagm~vj8K-rwTtGb2r+#QM+zR+TEOt-UMo;0yaUSqii0}|0AQZJt% ztBB_60b$WQ0WsXG9VqtlcGo6B<|fclFhI$|0Ae+GPYtEpYm}jBm6`H8hYbLQ89l)z zuoy`luvvT)e2wUp^953SznKG+=lQutbX|dwmjjemEW0$E$1Fa{#u2;3v>C?BjFweE zc6@`+^0p)Q5EH?*q>&CQAWdpYkx?NF zL~3ky!=*D6ZCocPj{jLv!u5{Ei5i}SK(d5#>p>o=ch?gseO}<}mWlI@+MgJNJvenua zl|a^huDsx7#)9t0?9m^22dS=bKNhdlz?|!qCwhhms}|w3O_*#uD;aNEu}26)(b5-*6cf$0!neRx;JIB_3vK{4V%?H=fNUQjZ2J; zDhi;|nUn$fs^fqgg5TT4V_`wvWmeQq{mduc^bx;$opwq}f_+V;pSpkdH86zP_B*)R z9Rym`*b5e`jLh9RYJ8CS9kj3h9V%FjL;Bh<30(VbezEHjW|GcQ?a1qGC?;S3k1v5Cz+p4o5PpYO#U|G1-9hTBv8#TQ0Ea|bH=@o_r6VQ};<|^( za$wob5(?wqTVI3pgDP*O`9w&NX^1BP4zDiePgcE$e-$@4PJH(`6K{c;o-D z0O`ldN9H8?oBb;{aM=b?I(7PDmB%W*wRm4CmpGHQ4xm+GnBe6k#5-%FQx0`y)0o>! zVgFn9PXwSR8U}3E0F#3&siyxOWJdqh_$rLcCP@t4NO5! zR9EiuKU88^xx2Iwk`CYh6!URWtV}PivTIvZ?67t4hkEQ0N=9?97x(Z8J@WH>Ui6Py z)5&6g!JX}thej}XKn@dlOLof1g9>tyx~>y_96k+j^wt&S+hbw~yZ@yuD7uUp?0VT7 z1gC?;%`x_b6R-HHMUHQ`r0@{?ytF?SDXo3Ah@3N|e?t%dKK7Nibiz^$Bej^Qqz%6;h~~Ho)kUSG#CQpVO^jgQ88PVP8=ta_l;!fBNd&`^J4gw z7a;ia_gfiSNw-OXK1fC8pXAGi3xlBsQJve;>BsAgeftSJxmTRI=@rVruvm&iOo5R z_9QX+YJj(<@&pwjZjN32%0>{~{2ISQY<=yOoyX`|iE;WYLJ~AwR!Q#{sj2#EsEZPqHWFM_ zRlVJos=orb`=`t$?INB!Mu>=GT=YrCrpM@enZ*+L@+j`<&B_;_$RZYxkJMyHdG7Vv z|BxRYogQp(rH)D5h&!zQ4A3P1=0mkQ%`%)W4gdM~yLuTPeRKKV0H>q+&7EY1T0^gg zL?b_$Fca)rs_;cn(XThsl5-1bOuMHEc?*(jK=b;^Hbhh6vTgFf6|6}O5__3%aT!8E zoDnv48D1M;%pNF6U}K^cSsrVwN>5yy5x8f ztxm56*RIN9vb@`|8JqzNRr58ClFnH?Qa}CD=-r8bvR~1lqWf66oO2}tfrLz3Uy&LO zD}W5XRA*`bElQsS;`dE)`vQhF+d$8ciWTr^p`_x~5f+O(9*(rB}uiYl!Zan0e=$T_F z(0(EJg`xBlqFX2vzBfYhP4c1`Q_3S4;~4DtV}ty+si)@b&PVyPTJJ&HlF`*A>)efSMma~b z$LkYi-7_sMM!)KhrZ+cKIX?x1qPt{`!kTJ^o=`>am6Dqu548~8_?p{??87)V>Ur$y zwcS_q{4wEb-52e{-|t)syt-7;3j@+?}_WYlp+i9G7USZfEp^QPpO#3Fg zLvC(tgQ4@FDsx#sZ)m04B(HEfD0!!T%3{A>o|O{SE;vXS;WCdcaJdJi#IVyV_&WJ3 zdjV=Q%4@ANhk+ICG_M44iR%n!dTDNzuJ$xq8{kT#!sh&v2Dt9HI;96Gh3xq+w0HrW z9V;oPJ)*Uf#hakwx75}UVsH*sJ20Xt6X~1X@B0|Ryjmq9yO_KRk7tFgYEO%nbF!lT@WLW4)wmP!VN!eMNsqTDAl(4)tOTW!93 z8B^!vMA2-6Osx;q_R}s3?2MR3FuM|kv^-K)te6A6II^{gUxX?=)-|@9mh4Ng{dBFH zlHZbz*uIO$pe|#jKpv&W-xNm~+9iB#>9IYroU`G~e|P~%^|ii;v>wirP4(gAiRd

K(H2$az4OCzGv9e-Mz*M?n;83Jbh{`jIzcx=K9r#_&g*`-JFcG}A6-XFwS z*S#m&r@uP|Y}&$0U84BrxbU)wE`)jA_p?(Z`FI@R#gTQ6bq%uA8OcXZs*GVDGTu?s z=dRI})4r2O^K6&($|IpCG7l2n;k@A)ne7hC0QnnWRY^B_KH0;q3ygEFI}N ziJDSS78s4TfIkL(+fu_dEXUbW0B2Ng0x^LnCi~jYU4*O zI*SUN7z<3RuUelEqYmNJuyE2xo!_Li7_PE2uHodQbS`w68%ZqSFVxG!yt{^1rNn>s zqm1k7-Er~@)?qKni*VH=1H}!~2J6BWSCGk*(BwYit!YS)d4N46x`_ugNM>jN(ptfg_S z;CBnqVL-bRV{FPs|H~!}`(l?T3dwftL@3my1IKG^J@CfU_x0X5LWm}=zSjl`+>Z8D zW_-#POopYbZbom{rzjE#?e4kVm z$P%oVU#RkrQU6(XQ~xF@T_Npswc~nD(ZsbmmkP?I>KXnRM^g47DV-MIr{}M~{Nc;_ z9Od%aeM6P>W46;|-u(x4t!(ye+MF246<;LAWs9AQopKAdC23hV_q8!SHL@QKt8wvR z()Y#Nj|p}UU&YjMtd_fh3a>TA6_A23P zUwpD%g=EGOs#Xs*cfcmZDW0b1NJ59NR(R>QrQ3(MS1R|g?cPhU!|YK6(OuR}vX`K^ zWq-uVxY}IbfL<+4ZQvCJ@!mpD!eme@!*K;>bQ|_$n?>CWOQT|2W%ryI&7tCDG0w(7 zyXx4SPA%=yL8|Zya%o*&E1Sslga-B6R$fZCHYgRA{WWbXDl01Ec?XP(rZFR2%UFO-X@F33&;maje z=vq#4YuQtM|G+rGL3KFH5M?n-_9~XgIEPU#ipA8mR6* zucQEx*El;?rf!*3Q6qoj>S&?b%&VeLT6^W14sKn?1A9EM5f5%blMSCd7g8w6sE~ETQ_wG8cYU5SpyM`-# zzM!e1F?uVUKhSpr6E4|iRpwgbnan~aIoPl-v? z)g^iPg;+nIc&XrIyFYi)I8n0iJJ~49W+GO8gptfpa>u%nQNoE@!=Z`;Og1NWBt*~t zBz@=n89(P+XY}f%4y+==5nA;BaUmtqsr#r+yI0st>rQt=i2H8Z3XJR2LZ6h8F}$w) zHh2`J3?+%d;Xba__p|I@xfiL@qbdk}PK}8qHz{?X8k4$awKtZ7^PtR5bn<0@;iZbA zxe@$hp5lw6T~huBF}XcjKjjl(@zG(_XH$!KgP5eA_?u&b!#h*@8+AomFV^vHl%wEa z#t-}Z!5qHoUs8VQQA8InyEZ`p-s#foGj+H z5S#oZQ?ajVmLfyt_n|Eta{LbeKgt(5C|~%MyWvj4;CvVuGHXk!U4iBBwBx>AUZ82r z;uCzVkIfd0453$Uf99gWVIsY+d6N!{tl-1*YwB0f0Em0qo-MuEk{iZ1{E5&^PwFqy|V_$`^<{W*NJdCd-AYqc|{vprd+cB^rG z+lSKE`L5txws_ei~n0h*kP5Lt?Ka!>Fh5=aW7D`eXw5WVs%B_~a1MT*(mX ze5snVZ!5OE>s!y{7W2t|)bx57u!@K`!7=sZ6Ls5)UPcA&B>L@%cjR^PAc zOJ-z?S72~3;%}3SQNO%4_)bA_Zwe~C&iiWr)fHU`ba8{Nc@@lOQ2fvK&3T`6k=KM! zT4Ebs`(-{P$Ya{8)F;6h`CRCC2ul7^`cpm&N<;kJ=IQ?Cd-C?S-S4W0>m0==`0Rpu zD2=Y?RZn0u%Hcdg)NgsVvU{?9OL`q=EV&L1cUF3MA#<$9gTB@!Vccr+BBFx0OEPb^jEqyO zYKz+ED`I*vLHg{wOs5n0C{?}Vxl4O|s#PbA1n$WQTSgWx6QF^Ayv(X)|7377k=N%vSf8x8 zo_FwS-Ez8mocIF?Q{W}e^4_Z1U=h{rUdbMww0&w!ore!Tjz;|MjCN@-b!fVq9AiKo(C6o%;e*xLkrH z*OhG3g|1j!yFOmg6bLg;*}n(AncqaT=88RPeWgFOv8FjKG+kWVlYVh;6%OFIrz z_W&Bh!vg~^FRei1?zjvDje95*eFU%IpvB+2%!C^Etwl?6c7HAPd)!|5S64&zo`0qM ztO)!)ZvUT4p&}DMjW2F@Wlc+=E=4}v-ecHNfT%lx|3g=8QrTe&AgVup7~gsINbhtz zdIec$aF7;krMcLX5Pw9fGk8CgwGM}y8Qb!D+olu=cE1&{C?azfZH1AF?%v-kI=e}N zjH=0=ggYv^s`(@>+LPrLq#G}9EhT#o;V{2VMrIRWjh}%mrmJ9X8R7MOztc-U4;jq# zA8%bqIEy|;HgB}levGs;7+P!got*+!oC-8!4-8_&zq}_-mmPg!RR?{>{b$aAuH@J) z_UyY{HCKeZeu?pKd0n95L@}fJ-n zwLyd4V;-UX(+(1QcY51i#y?L$_RkZz7C?kWk#@Tz|RT&mS0?6#dQdY0aBvTkL%v(SC` zLHF5uhU@-X)w2wreRvZa6qAmo7VXVq4D_MQ#wmN-`-C=tYnKXhN9j?xI@x73L(b;g zY`8?}um*cv_2|nv;SQW|_6rr1g!byFE|RIHz}q(J^Ei(<;sA47{%~+7X!0#Z3W8_A zMhTbw2V2kh&2niQz=mXh(;7Zu@44c>Pu1+34`2o4awk$Ggj#5*#HjOao59yc3~(Fr zU^%`L<0TPxiy$s#cK@PRfqg%AiBo6IdxwRjjGF_~e#w*dZZ&stj;S^VGUTe6A0{t_ z|6FuxIMS&*L$fuvqr3b$A&<@qvEF$=Y9DHM$8oJsTeI9edr{096Z%BU2WmPFk(SgR z$A|@*T8eG|2AnzXxb)d%1IgHTpOaMG`i^x=rbB5eoia1{G0ox2iB5La~V!Wg^ zkUw)UH&#A!rbG1}F6Q5B5d%;>Qx> zEA22Yx-MmcTih!a^LywTd}pKY-K!dY%i8a4f^YeH!!V>@{QjY3@&ux(#=Y1Z5)HU| z;$K}<@i28%pZP6HpfQO2{!!#Pi(4JnPdW!_&v)sGF2i4+^^`7}61B))`gR3dU~1uM*FatgS0!Q7GAH;0!vTV z2%n*Sf+k??M10`cOxA5hSMfBdD8mq?ZDfu3-baVEF(r1*_bi72xTEIVci(P&`sC|< z^$J-4S0L|~8G|nLxtuSP0{|U#R=aee{BhC(wQGN#TR&&mg&Hk08>RnBSm#*#qbq&Q zkn>eN8~H+g<0h>#h!%u7i1Z)bau>JN6hK`+Rex~*mY;5lOJ1tcUD~gk)@f`+Ho%q2 zguei1k2aL#L#=SfDTy{{Vm+#jukfO|V|5AE9;C(uiiNTUcVY8^C|(e%egsJ z{K;e8W6?R*0$VD{ecQZTi6;IZ8P@|IWAeCjWl6GC84%jfU|UYosy>9jL*~EaH-F(Q#jP9l@YVPx*N#C@hepA^kmVWeN^0%}uWYu-U+^5HIZ@{&4PV1Hkuu zT=ldf!98<{XU1!cwXdP=^vW8}{}AhoMG+s|kw-C)oyk_0zW$C1i|BuG6yn%f+0pyD+lf_^t6_Ca`Uq_A?Ntn=Xd!a^qH~2k@yrLyCR{ z+0_3!S%13tyOxiH<0zRs?&=XaaC?kkZ>6R5AaX`v5Xdn}dqrokEHRiYa*|={@Erz) zv(&lM9_^0t{E`o;6TX3&ko0oO?oS2U0L32wxGI3(dbT|cUNb;;YQl!jO1gW>eZFUi z1leL~adgR`j$6CZhSYKIl!!7$2@I2{pDiRU7yppTOx__5d&cpnid6ekMas&&;p!uY zW9dBXt-JxdNcq!EaH|PLy!_!g_#6G;pjvPR2$^=Q7 zH@d>1Zmw&w9+~YkG_hVi3gR18hNm*9OKExlU4M|RWMx!p5;3?bwa*RfK0m?o(5cYO zYfGYMaHyl?Loql5z3vk^;u|1%@gU28amK+f+P(hg0M3B(_i4~V$6YK~sToUq+lO-C zCZ-NHZDTW3M_-JT`ed4V^bri+o&ui!i3mJ>T%o76BE()CqFWNDe%E zeKzNfI1c*j{ww3{$d*)M27ha@xXfg{vkcDaH&8^i08Gn_-3~(Fs{3&67t(ax7259- z9yHhtkw+~u)Bn)^({UC4>A3!1;zI((jC05TcQa-3Mtpe>ef(W};!k7OwUaQY{KV%I z<^D8Hk$;+|2SkkLZ%W!3(xAh(Px;U*O&3h!yf`pIzvzF&kl8AMVAEgZLV``;_5TFh z{{-9rmW=*y2sRm%Q}YG>tX{VbyTdi4XD9=}o7{N9S>+n!31z5--AoLbq%&?B!sNry zMJVB^z{J;SJE#8(g! zT4&Z2Ica>V5&L@gBCdeRT+JLnE*V}0usRC2xKtCDamgJ3@f-!W73FG9z!(Y299C6f zrk9(y|9S_!Ep3A63N(p*Ygh^$8!(5}0n!e-toOR*s;~ z9;tVG_AgfkLm&AQDB*|JJ+{1Q9zv&QOBQ%RjY&l2VeISGtA?9#*5CGHJyf4ub@TDM zA9qmT45u_0xuw+qsM}or0>!jSM?(p6iIxo~j=bPUHF=4%cFRiu+QhY;R0~Bo?Y_ck zcQnu;A1Z6oVX55ral3#F+RYqCY8e_>%Wg0#$@(`i@n5Jn`wh|1M07I8($*V^XC`u-~gpD9rBvERwgC`B(+7R7n zH~;56&Mc2#rz>EuIWz^auM;Ho!VxeM(L-8ciG+s9C~op>Q;oonJ0IZ z2gRB}R|7CvXN~5~j~Lk#E(~sK;7|DwQT13lf}h&TtUt|t0rBZ|rM}_UMGIJ&3Q3lW zY2#IP?=&l|Q&uJ`th(2~`<7iyX2^N1MN~+C#K1B#g0<5-QCa0oikX0y(?9X%6H<|S zb<(8QG2xzLIXvYT zR6G5TbSd7Tw0zuZfmI;82 zp;>_|gTj~C9@JtLO%|L7Z6ui2T}k#Rm1R@kxUbX}{X;~uLPX-rP&aGay-RYhb_HzA zzErx&`k7gc<9hh{2x%=6`+!Gz*JC#++aues8K+x& z$gDJRwXa#Z?@+p}qRkkn*X<#yLcPO*==A$^s#kEU)hmhe&!Z3@@a#g^riF_mm??bw z^x!Kg8Oa?mJ0gDF+Qv!@;o8-*ZxFoeGP|%o>b}#$yCAJX$MNz^dt(zNSP} zWk5M{W8D8@`0XAH8D!>|HIi=hjp25?IQe;P-zM2lnwzU1VL#81f(0K z1tgUg6e&q5Y3WW82~h+jM7mRuE=37J;;fgM?>lGj89mp`Uwh9Cmp(l2dRE-)UiZ?Z zi?nc1c@Whq_E~|jwVsG#=jWfL(I04{Q?d`kQ7sHrZYJ9Zd7SP|xZ+c9-IuNOjqIs$ z2o$*5oHH{aJcIHS+6*{9d&0dl#W#Xfa)G?W9C&F=3CYi|{M9}_y7=AXa*b-C(+2$W zf*@ei6P)uD)rWg411;j)WlW7sqmaL)3*8blM@__X>GgwrYL+zjxnVHDffAns8^kwq zt(pq9x0fLXEfYI@s#TmXx{T8Fkb?4A1=)>m;cJwmr62Z*zke`I8G*SVqf}X7No~F$ zMK(Jwx@8$WkMP6mruEuXwY`ZTArn}yl1~Kf!&=J$n2e5eeG}M}!$j0TMgC|Nqb%?X zfKFlKCo|J3Sk|g{qe0?$=%$LfDJ7i)m|%xb$H>9Ef)*T%JCUz*M+{T+`IGn<#kE2IBmx*gSpgr z`*mn;zC861nH=0ZHBP(#3F}#kM`r|AVH(DRDe&XVM+a|?ECR%~*C+J6R`jOSa#Gy$ zmxs$TO!`a1A%k||f(*J|-~w?b;?+r<%j*iHonq)TLY~c*!H+8Va0EwwHD3;f%>$a} z&2>_ZV*R+=?p>{Wsennno)RzaRg+TET^FEyc|&TevcyX%l$?*DFY3~yhDFIms2#kn z{&(#lzmOUqKyLJ^VZ&J5bU*3&6B;qy1K8)LZcyVKS^G+a;jC`A3G)!Vt;A(e?|L>` z<5KW`xJd8P)|I30SFB-rBH!qZH}3mN)l=~2iz#lGuk!8g0NJV^MjA`R0jtQz_xyyh zhl;0zUW=6gZjIVG1ulb{o08-bobo&eDb9X3z0KBBOb&Sq`+?uKt~RLSfzxBCSy^={_IvRF+8%n6V;E? zSW5f66Sh7Myz%jIfze`8bM|PV*X4E-A_vmL{dwwJU6Zh5IRg?5qERVTW>r>lUtDRk zaN(AX>MtPoJ6KJB^0pk=d}?nKM+;;m;3FfMzNzeU$tnl()F6Pqq4%zt*QoX;j*h54rSS-i*H-Dg792QVmdK)3+v28+*7n3k1ZVi{UO{um&J`*c-9s5c30+381V-$1UX-J$kHiX2;nHC8M zoQ90KuX$n|!KZId!EnrkN$e%n;U9Xwp53#Lqz@)Ag?{Wd-5!Umyw(+HxzRf|0{{K`!XwIycS_Gs6&r{iNbexpc?b8#Xj$#$%R=@{Oq16C?zgz!o%|#1s4hhx+Gz|*qO?Miq*F2- z(-?KTAZ_twq_44ycRrjNHf|UexqxQLwl>X0*2UhHECoM2i%0lpUMbBZ0BjTv?aAv6 zU8k5{wqb1q;$uj2{itrT)N~>Xo_qKWo+Pg7R-ZF)Cy70m-9nD9}dr6|BD}6cPDY58A!{%Nz zGYWNVi=MYj^Esrb=er+7a#2rD7bN`5nWr??$DaHur>S!r`_tGncv-SojkIQ-5*Crr zSUq@-@6~$peV4})kGsbM${^ymWVAqjt6DQ<%6OJEu|E44`vmsmm$qZe+V_tZroYfc zm4c@0vK}7L%bZ`Mi`EN$(0iVySkjV@Ba(EMsRwq#=D&IM8st(25b;~yUO#3Xbn?ov zN*k>))_KAU89J-Vc0%+IrZVC^#z@L9F^yljaOB)2Wc{d@3fs+cDOpf;%=q;vc652k z`J*PXLvo6c6Xd{*8&Ic+!9&koTkZ8Yi)n!7QGwoyqzAk?CueaEEvG4Oj#gz~mO z_Tx_b?(eGFjpK`l(}-e2MKm^&IO9#fzax@|Vly9Z9pGy}i%xj-wU(H6o-dV2=7Y(= zR$XEHS?2~4XUUtlxBYQMoJh%=gMxC7V0Bg=j7Kde5jc^WFrEJ<>6vT!D5+b&WRnpy zSzyCp1H4vQ#H8u5CD-**1^N|VI8P?4Fd}=_nZTaBGAxN8Vdxs)5qi=w5C*uckBBdWUizs3sHi@s|J2g^yB z2CkEGQD97H_C91Mpm1w7l9ZbW zDtW5wx_i{5sv4%KcV`W$t6~}~QC#hyKi_mY@YU8iVfsT8CeIAACv>M1)bR>3KJO6D zClzFUwU^}TkMcOfl+31+F!?s=X-G?Nv*Kg7?@tsvwwB-E)qg>t^^`d(Rx$Pqk)O+D zy$UfOcoH1lB_naX+A*QO=cYWrIwAc$t-!Y^QpfFxE?lZ_yx13C~bt(>j2HHj8t8I`u0sHsE>&P`X+!)q4?<>cd1 zA*%t9_PZ|1pnHU#?9S~nUwzCs4|!$313#dhz}SSeFgR>VHe#WY#B=%pvg46BQ|CL3 zoK5CDtc{f3+wrdGMu7tEYzK}Odz(wk@B?Sx+@4RJY2`TCG-dLg;>4Gu-X$&e`amoq zdV)(wQsRMir9IG8$=JCYv(ji=OHLo?H=X{YyM^Hzn$u+yDHdD+y~C>WI%uar1mddK zg6LF_WF%dT0)r=STAI@oZ6KOtKqPCnMA*Co9-q@;G0ldkJ5+z}r3gPeN#)%F3Mp#w%e2s3t9?`2PQxOn z@kvNrYCG(NmQ{+M-8~_Ki$5kV*iT*-v&MVsn%JA8LMv$6gni(VjqGQ;HxYh(m`lB_ zXMT*RcT3A?-IKY{2tWbQ}Op<~K3qId%@Oj1&-6Ej4@_yv`B zRiQ!cAWajiL7>n0Ng2Fp{>tF?epNCPiGl zShy@e1L(*i@L+?GWR5GM13y<@mm!sr7IHPuJ$d*0LIDN8`640D4==lRKrxD?sYxRj zr{267G1!1_BrVX1AMx6$=`6;L?gPp*^VLnc)j&sHlD1OtJwqjQBNUQj_-I$fxr2>} zRJ*IwsK&EX#o!k|PX>RZ{*9s{CES|PSIisi^AZgtY>R%?B&Ix=2JREk*!hqGU+7-6 zC9ncm{f8P%`CqMVL8QC^QPqCr)cY{%0`6Cz(8cz$z#z<86jFKrfMK@F>l8CoyV;N^ zUuoIHAZEP|e4dy0dF4f9J}M;%sU`zy()CTq@!dE{3cpgi;(=RZtjxWw2Ite&OC)iw zbWohp0CQne`=z-A8!kY)K9r8fXriDcwc4Nel;*%snh8#@&2y6l_kS#`p6O6z1W|$&XKrJoh^@#Snf#UQY2&fm#x=4qq zK<3MOKNXNz2dqc}Ivg{m_tOKfp#?$<>XeuP7? z{Q3~1%ZPxDy8yM!dqBqCU+gV6QxvZNeb?h}S}nl%MKfzNu9VbQNtTr3-4c_h3Ti_#FL^z2s4an*5wLHVXtjJ0` zHLn)z3YC7<_p5HV|cKbd*BF*$wv?Ur*nVQ-TuFl%qT4^D4K6xEC z{JOIGgTvGaX+mfFHwQ0MQi(z&kY%T4Z^N$~-gkkC#^ouj__UzJ20^t_ zDo5gM@@Sme0*|Bv>ehHgdN9(gp^xxJR}r*ZHb_R2%wNsXg>K<1J@t1LM78CmN#VLX z$lv6iz&OeJq%b3`!cyvDd{VG6-s#>}KfEsm;BpI)w*CYp^xJzo01JDGa%kR9S%n<| z0P=cp79Bs0rz<#M6C%Rk5D*%IYPUb3gFM-c0hmST#&3q2i zfJC8fumQ`DO+7oe+G)XB6@ZDjto0TujqHy_l#(5%|)t45Uma zYFu#rih;I*n_+`!Zrxp3K{!M}Qkh6X)2oE%An&-jLYZ`Q zITd;c%jqH9En`Wi=$Ff4dnYqiwWH3z zofUA)R@J+MlB_}|Ua(Pi1_)twE$iOOpwiK}Y123;FqrRHIQ;oV+WzquxlYX_=L692 z05Ez67{f284;Jd&!hmgKzPK4n2tO48Vro+<+X##u6Hd42>b6y+c|c=-Gq-xFFQ)+X zIpC~@ zYiFJM%(gUzJ<5egc~>3U^(D1le*VU~_|@Y(6m=Trgn=+TsAmZZf3HB8EuM6_h5BC7 zMC#U`PlxXxKb;FM`#8M_JxS|9Q((>3%Fx);VHncb4DhK4M5R>p%f9VO`BniNPz;kV zN$!ByN*=;9$20`^(TwM8#_Z$M6zQedO@N849oF6X!AyvmtlzbZBo$?dSEVKm^5cFI zQTrLf+Q_fNT2!7oo8-I9@7hx68B3G*KcDfqfXtUXJ+GpPfXvHKLr!#s{y?#pg-;O~ z5ynXvq9$s;)E6{UdDGa#+K=)NR4}-M=-VHn#KA$u2&KANQm4jfCpwRKbX$>TCZkQ zzSXTHYJHg2m=pm#(ZMSZL89=<>zf^Cg2r^puu<~?9ZnmH47eiIlbioEz%Z$wSMYmO zy~OK|m9b>#xgqUgs?9|dB4?b{N~|o@u)ZK8fUY_$YZFvCL)2QHcFp^7svsx20|;r zsb&SX)Y954@Og)>s31EYAWO-gQ{z84ha?_PSGfPhJF!-ZuzS6wDQGjuDV9*|oT&n< zl`3;7`Ai>)KyG2i&5)yNZI;^i;bXs*g><*c!0jgqiNN6}oyv*`6W_B0?+&`Kn(nzK zFAt9Wsm;H*h`jTXS50k5Beg57HAZki19BJll>BLGk(+Dq8o)AVr%=l#Dv!4aTm91X zfZlF^n#s#aDJOG&dje}QH4*9FnbcRya3cxe`Jpi+J9k8kLOVb@J?#{NltcCO^4HbV z?PaD&Ixd}ZDINv~(BjMojfu9D`_Q^xV;}-W$U1nWKE!f zT%koA3o;zPb|^FT_4tv}?EH&Ix`bMSgm$3nHMic&>tfKR+Gc)KK=KLKMWD4DSxYWdY{FMW)V{u+y8x6<1$A5UNnt$Q3 ziiw!U>n%tl?*o5ttr^iA(d|D&E3S`>_qqBIUeYh)VfgTBh?98{a5Bx8a34F*{4h0? zh37~PzTf>Z9gsNT@4k6_6YKxtCR)t=9_=oc1~#QjhxEf(xVEcgJlYf|L(lVT0p+)( z-DaY^RLKY3lZpK7V6YqaJO7M@F{I|(H*1}j(s_rdyiY=1WwS%}XMmr?{1=uf$0P@S z7)f6G7=t2CC;H)kjz8_wpR2kI)6)F4r@pf`)bsM*GR#$!40yA_p;>r?-=gy-khZx* zD;geR6#yK*IHm(GPT#dKP~|wbuN(a1++038?55u)faJ@OQb>0B4W0JJbzxY40xtxz zLH=6GL)Bof#)pN|=mt0<9XwUgvp6tpnQ~W0$mt6Si2_zpewnv+FP%8{R2~*cA^#6! zDPuBSk>P9I52rX8piWX54jBjI+ssYp@4BpBVh-hyqDP_XrOc5_>@B**99EJ$AeU^j z@dT;?wE{Nar}TVTfQG4TI4Re=pQHL7(Xz-6G1*p+uhJ!TqNth;KI6iTL??q z1dl{Yn(f!w`309=g$SS?6q7|h=oqeZt9T8QT4ICT3_Xj)3pve`@3_CpsQKCb++Ne| znAX(n5Zr+hS1%tDfXWaF5-&-TQ_WS!8L+qnUg{oPhw!5{n*_g3ET-lhsLmRYDkbHY zRZ7(LWC3EPCnE0x8R0rWJTcDTD?+;XvGI9pk?~2fO;Z%hPnlVYg@h;qXGqNZLL@$k z)j^b@q;dLi23%`0;$;~ZwAqOfPf^oe7$rUfiFlmAbUW}WH2yEsq{G*>jCCSCjU!97 z6bji*k);2GdZO~4`lGC--}&P*F7uX&ceappjG2)}l0)V)GQk_JV?2eJr<|Cd9oX>R z>tJa*m<>@Ub%S*m)}REu=M^%btCRo7DVFP^E-xdeV47IAg;OjrxUzQA2+qr36F2Yay%+`4mk@7NCP*z zr9U&N0zJpKJ+4f#Ad!TcIy?_xspXwd07ls6!hFfnn06NwS_d>Q zuwD@+K0pFYvyArlQ%KCUmZA6w^n$WTVUH7-$p~iyU^6 zhF0z7Vis^?j_c*s&X2cfh8IqncMun`EKYiUR|4aaafpot-JKq8bO#`)#L~>ToCdH2-Tfn|iw@$>X}WwgcqV!k$0A z>R$aOc;fgh+-Ol528gwUNni;8=z_+aWk3Q`$2;#X01Bh>yX%-FV#1EjQ_o=s&jQ1> zdU6nbQ&ST0%DH(8EayXC`%j=zdP@$0!{zek+o89!5i|qIwB9c{AcCg&<_$tCbP5c| zAvk_Us5F^7@8C+f8D63C0Xs(Hd(OhTNQAC)jUS4q=f8%K!LJOVaH8xwlZ18i>i`ZS zCAE`mvQCI_4xn+*ok(pCtt;<>=)Z6qMJ>*H1<`(n1r2=}^DphE>=NI-!-oO*?k_p@ zUOz_<#^u6}jf5Gv6U+!U$7|X~os*^*X7-v3NMwqo+d~L8^=t4JUQi0S+qA}_Ky9fR z$c`%CCL|#;)+x@!erPrFs+d9vND(_9YTssW)59^*b`4s zr;Rd&i7MACm!i&pLh{5m{^I_01i)ohYx@GnNwF&8QUjE|_;831jwAQGkHp9%2Z2}% zve52~rLR>VqbFZ`tn{d8OQi$j&itM^4W+M_x$%1;!VZf-f7w`0o%H5$AXmn?O`WC^ z>gGo80Y^h#RBp}nhvkw6SH6CaSnkEJN?=MAlUmyuUXxsSY!j^smaG0d%LOidCiK}G zxfxVN&f$^(82Ja3%$^TW@&#i?DJzm~LT*8R4JfX&WIGThsqI9KMk*P~0Emv8KM@IZ zM5?f}WB1}-&>yvS_|JcyAM{z`^n#W|iSz^zPWvG%xJB=}+%F%V-N-=n5DWk1&%SX> zfo-eu(;){STsJ072P_$yN*IM^-?v3VcYnZ@eG@_Je%6CSO9)lk>X%dKjvcQe5P{W^ zr$qmj=<;>RIJypz#PqZRVq|&nHOAwBj`*vqjjXv-}opA z&jd2Q$Zbrz1!JWAwaqjkZw*hI%cmjSVxB`@s6`QeTQrsGv7k4?Q4#?~rq%+0Q_<0itGg-Z^>>RYI{5g8s!K*l2geI29$A*C0+lQbRq6IUPeGS{= zPAUUX#yorbBpoT5&da^0;?ys?zOLlk*6})3j5=ab}qPDWe-&`VRhh!6+Ob zs_P@L983e~haKwTB1~U_M5aj3cA>cg%Pi%)Y8cuhL>kLQ;(9bP$J>rc_T-W0t@da( z_qopA9%mtc1i%Jbq9nD~W{)+j#UU0gNG~PBSjL1AvxoIrXBp2M-Jiy!JQwmX&4;IE zkwI*V>bpt^%Rp%FS!V}^WYK$?R-u+kXQgl(=oew}hXy6ZcW0<|2F30B&&;}KBTK;d zcHX#gZ?*2L2Rq+SLbfBU4VPWh8V9(mA6A4C95M{bJwn=tPn3|gM)`i7`x5bejaBgg zq_-L(Ou%yd^cg!^mZOVCD1FB^=4Im4ACK@JtLn?$P-?KXCm?|Vs2?Mj1O?fcWH`T} zg13KSJFWxHdGnYGEa#|BH;>SJ1n+*5@Q-vM%CcNFxZnjFm0VJ}VP>vKzqZQl#LZ8sY>PaeCS5R&RL|-GvwgN{>9Z19At(b-p0sWXB#ia# ze|!at9oK`c-eCrs<9~E9jZ>5+FNNme)_m%lF(jfpK_nOU78q|fve-xm)f)Usn_y=w zjNLiPqS1!&@=#0R$>^JqgtKW2ead{Q+^dF_QLJ6Agyw}avWIjK#08Dse=5AP`5Zql zBTPgAk&a?t!;~yninNeLZ>E0`Q>Ro7Yby9F@~ec;i$|g1kR(dM<1PZ~7X zf1I6+xXttD?Ay4w>;-eX>BcoB+MmNM&Jv!bjh9-pjY-Jtgv}jP&hACSWm3u9C{eiK7|-_bpC#vtC{ux7CnSAN1J{y={pjcAL_e8E<4X}+a8 zQfsVZpSZ7ggQsECNM*ZWHKDG$6b3!xTZcTILa(&OA0b_4pgrLQmjjhdbn3*^j^v}L zi2y=AfFuS&z32H;RS`<-=0z0YJca-!?Q$^hQE>I70BvNFDITl(Tk3tCS)979LA`BF z`@p>3UANgTmgGR~CE_ZXV(qf@Rj;5Jv9fqH&7Vzjg_);DhGd*USF5IEz5b`23uvXq z2s(~Cv@8)u{4mWP^$hOtZ70tLxNZF=em6VacE8On!*Fgd;|m2$0!`Oiu%!Wz z@Gl1Bzv&yg=Z84nT2-#PVqs`%?W{Zw{AkK!psT?-SS3;qtfkkubr z=0w~8NkC_W!;zfiLW)B__8&v!;n1j3|6M5@Pq)@dL95S~V7ESAt&1B|Ebw^DpzNGL z=8dHywc0A8`he9&is^gJ;Fk;LyHZPjqSU409RZ+O+0CF+cCfKYx++N)5L;V=1eb-d z$~R^RlzD~zUKa>(m4%L7z%tSWgmZG6F7mHmq>~KXw_y_)MI^g{aFg2de z-w8-sFB62FvT2ko=JA^g_O{mw>rHiV9#9aIirCTPWNH)~6iRJ~4tq-OaMv2y9p z0^5iKZV>|WcpE(ECBtHuZ3&o;Ax*VNa(FxsgBY?Ng(+unYup!^cXSc3MlsA*TMIKFNT{EME9fC<_c zlG{hw<)+QJF3@{gte!_oy^wss`gJ=cJ@FuAURN=F=*Nu{y!8iav&A1e>WAd+HyyWz z;6hSO)VOYRGfQ9a7KIGt+3)I_MpT|_CtxxaA#!(yOB*isXDd;PjoKiFJ`8jg0H0 z^JJ$fl2{bJ4^F|*@ROF*3c7w`8)2Ze{b!py>N;G>mQy|W>j9t}Y+Baz5*i=;+^GtY zqk_!|wV3SeKOu=ysYpQ3{-iA9{}=6N?Exf6%3*Lk9x1&Co?O^Ig~0x%jD@?HB+h~v ziu+K32Qh|@8>>2{@k$_V_j5mlz=+xx_{s{EH ztxY%rdVKG>(Knz)r$Y*&@(<>nVEY$)$EAfDm_1>kD)Kr;@nG0P7cmU^8=-?h_z+Mg zs)T7yR;(6QQ#_LE>7KxX5e~m91_Eg%wg1Tu0@VF_MATf}?KUDo+fe~Apb*6wmWRTJ zpWtS2*xzVbIgmCz0?6Nr<$n-=F0wmFb0U<2U*>yI1+JYABVX!`?q<|{ZVhrB!syi9 zg~&HSW0I|9U8M3Ydai?8b1sc}Gm2qO!XW~%isu8yE(k^F#(z?TKWax+mR#wA?I2{IS~#-T8B6#x9zL9U_Pq1D>x(5KZPro zzYQ%=tmeD>`)`NK1AZnSSxnP{IF$VF3g2>(>xptMr~>;;4xd#;H0mUe6%`?Tl1ndu4M~(Gzc*V>E%{u8cbe93_}2TI$5%*yB54~OTS`cTyTLA zg1hfMowcP*d(x$m)_=bw-;^!@yjzwRp0FY@bs0n~I&g%8%R#!r1!QR8@4jR=Zkge{ z)GK|4;)0Y3A-2B7C`8;fnjiN(k&p9qh3s$Q44g#o|96s$sz7MFLl*f~--W=L6?*MA z0Sehdnb)_*FVB1#MJnNrXZ!;I<9+BHdqR=~L4O(kVWjK@D6C{j*IzD1Og84b z!)^x>_?g$fLCVqYmbv@Hk6Uc7832W0p2}M- zc>XcUDq>qVU0`*}lBx@|2spfS%&Yj`j6*85DLDNlk?pnnR__p>RWB2!d|Vc2orX># zc#%l8_r~9pxIQO;LtJ7~55S2!y^zLAu5<~+G%I0JVG9NZVMN0HN3ws65_GVyziwwE zjche_Lvl0*f_SG+63+X4AQI*ROWGD!T4#3EyPr)e?DQOSWA^@zG;~Pu8`99RH@%Sb zFr9q+Lmbag`YER>+sV2SO+<^X@)I&^W+?TJM$YXPSj(QL0|&?a08;L(Q9zEOf>7x8 z>cWL)uxqc_P$JR^gugV6@mYz$?m&)_A%!p`NT2x%@=}{3AmjS~vM>MqFgK#rwh5e& z;X6aA8DJZ77l?!2mZ10keKB_1RIhC|QI><-@M)~uqL|$Y1J6)i9R3=whnF#b%mj=} z5sT=7=zGWc31T8`TOsgh|BG^9n1G?$y*6`QvMNM+`X$1{cl@hIV?soR{e}?%6%Y+E z2*1(jpw630Y3I?*K!kDfG%u9@mp~)B3nxSZOf`pd9fa%pAt-O{np6&6hB>u6n^C_! z0ZlYUbx3S#EZgF@KCK`i92?BYFK5>(hKa~hSzhVr=O%Bu!u;t<=|-U7)Bh&V5%$re15W&&7owT$T!=qqA!VFL+-rSi_mg)yXs zGXT4{Rv8F@K$RmF8;5+E&FEj>ODx<=(i+@L`NS3Ft}wq&w#ocZ^=^&kr#}EhX^I@2 z00p!OY+{=SIBE!omL%o?<}> zQVUV&l|L`4HbLZXm=CYA$Q>sSRNKcb0<-^V5rp{aU2lPvfRE2mW`oX}kt^r=Q)EyD zndfTJedGPw%MBUTvn>S5NQzGXhN?h;|!BDSMHpOroR1Di7;B4L`Rwsr<}E%mCTRTZEys`9 zsu}Wj>wG(Z>{`Rp9&j>n^Qr1cGd0=Qi^q&9-Balr8xS+danC;5 zy39MFG2oK#V=?)R|EM@ityAHhF|q=!8Q|*M`1#Nd(XFzxExj~y;b7>^p~mV&t=6P7 zSHVg6kN(;y1KqmCMcF|^b`_;~6xN6?Qa0o=>Uy$5i5 z)mm5E1uCpP?EN=KJ4a2>C!@m1Np?-3yF$M~d>RDMIm#f1c*9st9M>K_l~PQ0K^>_QC`Kf%Z_ z`%52)y!YD`won@uBA0hyO6=O5srref(_V+&3j|T#=-%7Nd~dEsL9BP5b8Ce}ahy-NBSDCFNPA8V-804p#gNJl{?# zFKp+!Rr>CYZ<=MWNXVZ^g&HMyIq6|xlbH(s5X_V@K)K1|FS1iHNPq2dAauh6!B_*$ zKPF&4`}n~F#73Lm`JXaN>_0CWsoe{$Fh1aV_mmi25v}Dkr0>pZ4E2u0MyF0S+DHD9 zvH*#e7Lg7j6wq1s+CRUS*mI8#i$#76U&j4}Zzr~^6$v^uj|ZHoPNPsVP5pV3I@Spy z`~^CW=5nKI$E;F6V9a||k^IP3PG*ngHdwxyp}OTf z0Ru_R(A35Jc$c*b<;zV?vlSvmu=rzt#sc3rEL;pFB0gvmO9xcaUM<5rr!z||%=I2i zSj4_jT_ITPn1-`{pn=oapS2-ayve%JFZrOUQMgq>ZPiL(Vs)e+sN%+y$1y z9$nTAw}M$uW$$Vli8RFkivtEi`0c?JUrYOZ3%LxJcxG&A*+vIoL%&oR0a;m(~KFp=nVg!i9FeGnobr1pe5cTi&^7C z)i}Qz==e#H$;cU1hmp$}+3&)y4c6fTU*wq+4obuje&)Vg zf%Zd*oU1`Oc<4+tosyc)Wk=9bo3j)dmGW~n=Zdzh5;-RbIigU{8=s@A1ML@R(G5h# zm&t&DbuL5lPNkqM)0bfKt&G#3leZvBr*o!2gVZgL?Ks zP;}soj<~@03L616$3SR(of>5_gOG-?{#^ z0(rg2jjr%25&&g#iDQTmjD^bxF&6EdP9iWCZr{H*7KNzQowc#^mmVX24w-Xq8OJ2k zr-KOv&U;Q-N({OCusnxk)8KXvv;!2MhYhB$^k#)58@wzbSnQqFeItkR4eR3ZM$Bdy zu~}+!9CYC1d(FusPNZ$>0!qYtKCcYhw=;mH|fJtFN&=FU~b!vQk1|AXAh-=Z+_;N`Zd8Gmgb^x|0t3pq0&axIZd+2Y15Lz2g z4hCWu*}t@lZ>G2Cyu|p&;A$%NLv;frEby-U;xIGi$nv?Y+>6jb6DC@Ry(h}wk!t7_ zRL)tvLohCW{!RCZgBwWsa~~!ZZYKiCuv$H4urN``A)Vx>Zl(3yY+U0Bc;@$ee)-JZ z#^ITV_x4_p zA6QGHRxe>cq?#PE!KNvm?^crM0LG}k->821?V#aT4jE25G3*+d!?Nl|KR1^ct^0GQ zqhr1uAE)cr$4NrGPH!I^XY+^O>+=MQ!;zvV>AQtlweje>%Z$TrqtFeQ^mREHC3=Ia z-o4K#y>j={1FC76M2YTb&3ntRrvc%Bko3xZVrBUV+78GF+Gu*^@@oI{Ygi+Pu(g0e zDgpZ3YV+F8U5v+Qjv_f)}34y*P?81xk>zcNrFFD@0ZVH9_}c#>x(wK&HTU z>$S^}3B7EV<%|vyX!6@H<6!YQc+rv~5C`Lw-;{3RqEnxmmtZ7VL@%@a@JJ%(jP2@U zdfwewUDU_gcLF8C(oJu^ z4^gs)z!-=0C&bC)E;a#(#PN|{OS|=GjbDTHi`nbYkYB_zOqaQaq_2cDj(`5l1n!3Yz8Pp?QSj`f9g)z$Di4|J3f_6yIp^l58gfI4lJu;Z!k2((i zr&;IbE$oQkcUt|;cH7nZ%5^YKKLZ6+bf3b+nN(>0LCgjW$d#;&R{dyIiAj8SmbgXS zmP)eE)`~a?Dj1PnE3pUQ1iStS3V-JW@3lw(8=55RB9Rdp1-5fdYpg^q#93jSU&Psq zKLLC}s%LCn8-(tsJOHghjj#ktSiJEHw)-V{_F=uaI62$)v>9MB4y22#a<8~%ncado z2ofCk_TE2?`s*)^+O9?7_6hU}hfadUu!`1kxI-uIUu1XfK#^*O5_T?n5acJYN5x{) zvJ_%6c`pIhAwQ zeqeD#f+qjp#wmbYo>7@x1koC{+mRZ&8KEd7Lka}VRV{s}L}mc9nq7)u;XtOW5J`^N z*WPe}VKLkWHLX@90{QDnPMr?ll&B#?lAg2~LfrB9Z#{1FPz@-bx3wQ5SggBzJw-B& zUzT3Li>Da~v~t7r7TqRc6Caa1L&VCQNv> zS_Ef2zz4a`hDkQ1;^B6||Hluz1MZK1!TxVP)1FXYg5!(9aoA(mT<7-v@?)6xW;}VC zoDLzr3kMfd-Q&&J+Cpk03WNyOc~E89^KL|H#_tsO-EVOD-44CncRnEZ!O_9KM1-W6 z^2FTZG^m<)cU~#Ne1Ru=A~RG!gd~S3Lm?+@0R2bJ~lZ~=6x7q zC_;ZVl+4@0Ux2_7MxMcz!t-XoyzhRIZsn}NAXG-JO2q6M`Ap?aDcQTdb|r|y+kGHf zP;&#_Z2Jy_1K5m+nF!p+Qf9&AOXLCPbQTXMAX#THbG|pE?=G4(HdoX7lwRHasT1sr ztKP~38DSR_zc5lrocpVTQ@fCaPbPz?lud3kZj!k^ZTh+rl_5@Ll;S*C;JzT6CN8S;@R#DQ+!q)6W_jmj<)L5YNd zIB2KoX%asvh)L?tZ)Uq7wG-#xT*{2F{cx$I9lB)3JfiHE(~zx{*uInqyRS(h<8=DA z_OP%#XKT??U;?=MO^7bb=ZKOaeW%chASpnaFPq7yKm>|1-+y@=6n}7L7%n09%HHr} z3laj0xXl=X#VD5iNkh$BX}njF@*u`R7~ET@|6ko3Nh-frY#$2&FzrbI<&MsIjxi{X*aN2+h;PO?`D?g_tra38a!iF zOOT|2EXH_$k6>}8LUB(4v0n{)o)x|u^lAi)xRGrV;&2q@zwxvr;rvC%YR)Ndxz<*t0O4TvD_fK~ zzLWGw_DjecR;DoMIZNMtHk7yQBwaW2;|q@00)71=q5hnY3erbvL8#RJyB1_Sw*X|N z_R4un5so5Zi}R!Yl`5&?u;;KiBIAv+&@`$x_Y=o3t!#z)$8!+(AT<`k+}u^lSbB^{ z&Ayv|*M+LF-ETb_89V^-+{S1i(Mk@G>=WR00Dnqe;5|# zKy~fI#ss{X$u@jZw4a_=p%@{CvqTl{e3|-kQwWybF&m9ay?fpBSeA&a*PSTzfNu$V8iAc(M|mc~{C>-f)2y zkHd&rT$`%F3q6}8)b?3*kL?i)%+TD7%B8@VWXt3?pp_jA^Tk1sUUhYbZQ}ZJ6LH(d z9HTC_x!=!iBS~(6oE{|iww8>SPWy6SC~xAHI8E||==TA+A;}2R9}8maz4{mG!2XapW#F~1f^svSOhbbcK*?RY^F|sMFk0A$lhOq ztb>`j8gZ%=e|4(8Z-kJ&6>9N`qW6kP{E);)C=F)5Aa|t}alZR41Po&l1xWTX_;kQO zb9`yU+~uB!$q2r*8R~4~aYpkkCZL$~MAGz$K?InrURo4;ERhpuA3X@Z^?A%4gC>cF z%fHXjn3}+aHo@R<7Vpk`N_(MCl!DewvL6u$*6|j44&8H%(r_$C2y?w!(8)~FhPk_( z$c@h}ibh*&V_Klw)uKKx?klm7ZL&VhLu7)YPUZOP?t2T5~f!pc`#R+5AP$DeZs^ooF@;OtZ@NMQSfhx{#qK zKWts}{UdV+{?cENHn57{X6|hY52UWhSl!d~AP!`d4HJ3*OCVU0B#dp=8P++r4S7YJ z-L4U?8OtGzgpvB#dU5B)2z4p-SgRcaq9sdQh}>AAJs5wWUDW@=gFp30TLR@JXMhHP zABzzyqZMg=M=iZ43Mm4YryqnrKrX`~4E%-oe?YdvrZ5po07E>t8Q|80FF_*spD0eW(}NfH zz8DHeEeD-XiJ+8x6@;ujGyVFh!SSXBM9&H7J1uh^DQ8SEA6ptAXs(R1TF0#%*R|20 zvRf^|P-)1U;GrigJw7^%PJP8`40!%1k;Es5TG-Be6a~8AvlQ5O-N9!)`<>5HFFrw3 zBoBOT2H_~{t`sr64rInj)d^^bWO4eAK~L>x()33Xjdfb*p`QZ&LQ-^lG!HztpQ~FI zVf2f>6<88#Pc|JB2^aAWTSWZdgjIFB6GTA{0;a(ieSv=(gi&TakgxWH0tWS|1hp#6 zm2x`zC9BIWHLEtS17^c;Z#8S@J)oeNIL@0BH>&Imkl>5-+N(WE6!dbRQk^)?2!&so zaP2}yc#1JrUl;{@zDUQ2P&KAF@88hCLlOPmLuqH00MIi&jZkfyg&+W*))g%UCi}pB zLV4-(O^G)8lt&+x#@d3T@G(?_0BpHxS;2KMZkFMg&rzfz%eV}?Z!$)ime0z+J+2#w zEFXP108c9YKc6r;wng`C~ zgAyHVmztZ+nMxdBqN5vspWM#?MuP&!DkWZA0xDoeQHCA=K zfMn$lfYuPn3jYhRmOV}K+nP674KD(&{ydE0BnT?no&3b&+JH&cszhgblcY#A?)}a7 z)xK7Kg;eqVuh%w!71_Rn4?vRW!Dt>-#A>-2c_pB`Syr);?|b}ka!xqyHxR0@aQ}HV2os>g#tl=d?XA7eMU~9)s;h z8O6s13LJ=!yP3&O6NXaOFmy%@>(RqPJrrKpUsPMBQ93jeM z5h%&84wa-P#Uk>i=dY|`WxW$$qx!tr#nWABH=)}dZcelWqmztNHVAm#Cs1T2ca^$G zX$Uls&eLKw6CUG7!JjGStdUz%T|BErhHl`nA60h_-5^RJqtYJ^MfTf65zzId9|C_R zPV{auVAdzpOAN)>;=%c<={tQCRQArJibnsbW=!C?0iOR>afY(nlZ)?vqXNaS~}EILeJ?kVi{s+#!N@&icD| z-b+Q-vu7-j9COh(jjd?C|Fy?GKA19Ki|-EKQ@d_uZWMeuW3!sUx6>{_rWcWesmB5# z$2Vgm=`e2TC_rt?&;v@Ja5*rph@ccb6z^Xnf!RPms;A_k1X=R4VraH3fr2$RJaU=e zJ@QOJ31qQe1x^!SSXAWD7mUk@vm_1#S1Y`mbw5ILJFE4Ab*ctR#QHlZF-B!9k#cI& z8M@7#4SxoedmzYmw(Z{{W_=#MChjDGnEuTIEP2JFJ?K~X2F~s|KHbtPamUM0Z9P@0qk_Dar!Kv5JvhL z*`U?@%Hea;zN4@l#!p8RiY@UOZ0DDuN{JWcpW7|LW&r#-xEabk5qZLUX>p~EwR2mfoZ?>eR&M5VL9q9Ik!zD@`l=lATk zflwQhF%ResBch~z{tXrW^)epWSk*6VIKE+bdJ}>`Oo9}l-`w9C&=Q+?1&ymDB_>EV z7ug!y-eCU$%YHfiizJf+#7-@$KQW}}kG?zv1UT+}{>?lFR$kc|`mY-;v>`B9rg-ao z%Kh}Tk6|tS1f<=H$KpDAZ^$eY9&#|-*0DeyBDB7mZXfU22 zTHIb8`K+BrTjH|Z@6hS+R`vl6IhC-Z)HwvN6`G4ye{nA7OA54ZfhO^`&?W%;(n z=dB~`2)WsL;9e8M6F&3RgbuV5LuG5&S+?DkFx3KIPBx1f@h> z##VxQFJQjdz$d~#0ZDff$J%&xU#^735o>8}0FPPXqeHFIp{`dEn58^YGAf5}eKDx{ z++j9!2HAokun7`-xVSggZdN;*RX>jK3e)mJ$)$<9Z|2@YTcsvh#Y#$*ec?(LdLaSy zYlNVw=TyRE+I{maOqrWoE6|armYsU@n z?zPp9H&HjH=cVwUEcuE7A?J`h(6v1~KwvHf%H6r~nYF4Z?woW{itAY3+kw9EZ?WQg zHVg=iPwJdNy%k3bj?u7!Bv=Ns=93*g8A<{T6~!pg_hgmFx1glHhLbE&C5%)T-FFtJ z8=HcNGsWl7tqj(Lw4jMaGd9d13&tX8<&|acqM_pu-3PMPtcMqz$rKbDlg|f~$3QMZ+P2i+T+MU}AYcQl8%jH_xznuQ2 z78*CLgwl$!oEy$-NS6&6mUsnIG?gj^Tja_;LCV`~H6L%f`acv4&Gdk>y%{uIZEGW;#IaPc`b8sQ7re*1-1Z*@T>J;h}<5u@UM0 zHt8vwfX}sWZ3rzcp7=lP{bf{^?Y1|J3z8z;NJ%5zQUXe+lt_ml4FVUSAT82JNJvYH zbV-9E2!fJIhoaIUN+|ix6W6`tdG@{cf9)~;W4!Me>&yDE#u_fJ^E}RZ%=xQXS+8=} zoXgPh6&3jsQZ&^)a`~+Hn$FCY&vV_E*t_(^cI1;EGOY@IO!sGGsY{hE6H;1r zM^>_&0_Yk;`@G;sAx21_jbcU3(DQUlR@oOHRJkT`Vdd6s*p+o5Tpom!`|PXD3-i2^ z4Isp)5&r(z=G%k9dZ3NQ8bLR@8i`cO#mj?Af&R1s2vvbLd7TSS4ZqPF6Lvua@D*iA z5~UIl1qcS+;Qw+=U%0$uc*K1}Ht8<8*a6FzflkTZX+|$M8>}ogkMXXL8Df=DTztN3 z^Sy}j0T;DV9Q$t?HDw=49-})?6!8%SX?gdzHf9ngJ0Id0D-FO(>&+dyRciR^4#Xzr z^1a;jTd~t-r9l|~yZ$ppsd25{#S?nv$~M?UpqI{-?sJ?eUlqlb4LL>j*UGr1^7G?^ zy%4`nPJvbfX+$2PnS1xbX@Z21la8hZd%Pxi|K%(}^OZTIKOHiy!R^ffEEGffxnUV(QJU8AGBU}gQ zE4fZIm23M{|FGLqn{ey=A1bd{XyJJNyB1CbLm5W)xZtz69;e)a8%@j*2qgs|O;rH2 zzn|$FKk4;;mm^sdimrHOEOv2lg066Rj^)D;*>|zI`y>6y%yvtgUtxLibF!7{O130q zdR}ayt7SlxRwJ>~0%wByKOZNIoyU17hixxULH3upqtFT(q?YEe`bpk)QtiFRNqrH2;1$Eax%vJ(&@F<7acGg!8AyzP zET0ulEq285YfBmqQ{uheI|*8hmktgo;z(0ujW8sdPY~3tCIaKgqLvi*hVZ&4s3Q=9 z&dY0}<{vI9nLrGX=HogQw!MoykTg9J+X*cp0V zt;kQ?%(I$+&pu999!!gc|Ej)5?f4@yI)CvfMxc#<5?*-*2i1Fnto9lgJdO&l=Y}Tw#kV^tM`Pw-(uD3w$rSlA7u5I@(`=ty`qM*r73h@5+ zeQyDo4<|O!k1CMeNMB{cwn!?6+408oa%`&Zf z)c=%50igN};9;R#08=lR1j45az4hOt^r7ha5P2R6|C(8B+7#pojXe1eWI?wa3O#gO z$raZ?p7lDYT4qK-=KIBosS1)M2b^Osh-sR@ewZJdRj15>j`-H8m_Tare^En@&lJO0 z3{5m3e7mSzg9@|x>OS7YJa4=D8~O7%za|nd2X$V{(E~W^4I*f;lc#eYyt;Woa%le% z^stEbI@46J;tv?R`o92UyXhE*j69GQp0?`R9I+R`O-Zx`00RO)+};1iF&<-j&0bmt zL1|*{+$+XKp&YRZXycfTkaaAaABGm6Wh!500QJS;ffN!^wf3R?&j-W%^Pm4<=+4E> z`X4wW%M`XCfIK1;k5b|xkbD;dzK`hLFv@6CO}eB>O%TZ*_^I~pLpNg%g`obnF%ash z5c`4dzzT%>UN4>*RAe1W7HF{e>}~K|GALhrT(EPBA$>Zy=+AQ@Vr2LxNVzbMQj3;LVPj8Z7D0}F*qLBmY|I0>MjVuA10?-~L-;9P+O7lM zhTiB4XXD>{QXrp1=7VM*2eUuY;<5iiTFeX=9^T)M<($7IAO$DQKlE$Hu?!9Y zpCjo-22~SsyuVgaIuXCJmlK}E>($6%wgARFF>l#!Qeu5Kt?wIcEW~WMiN@QFmOe>z z-T%kg2>++gMnXxA@1D!=kj&B2T>CPw=QqmIa^gj5bM8Z`qXM*SRf*5=!#tZgFy2`JYx2#J8<0>1ih z7%sj0bo*$E%=QcYnOp&tBQzdfxx^kY=~v~@F+dC)Qw>P}aD)1>I=@GLdW`0dM*59= zaH3wF*~WGti$~@xNQJCzqrgX?`ZQKvA`RV9@Kj)OfA+tS@Ba5_sE(T_=s~#wFlP&G zgf9Pb%n%0+5F0=c_ZF8U72_Mx60JHnHjbttKQ_(VU|A(GP39%#XT~jes}O48buMV9 z+>6E#<;6e+OgSkP%_2u8Fz%y?{LUu6L4BdRAZJ+^2Kh>(+o{jLV zO{|fGN2rdhXY$t+cAorTn!f|IpIpvJr!Ktxp0!6d9jmjVC zyUK7`0}paw$$zR=gR(L~ILZ+ZfrcX+X#hKt{(J1`mFK^c3B3Co<(@z$Tuzs*1^Jb! z-ghcA9i*JSG?SbU2BpZoA*rM75vV0(dE*G301PcysV_C5maS~rcTYQb)`z0C7m>f@ zX%9&Wcu52vD~MqPmD@e2x`2oW2>upL)A7%Q10tR-yEfZ8RV-^otD%JWZIO_{#0vMo zGb*78SrQf%VI`_1L`y$7k6`*t`3D5+v&^a$3Uf#wapSi52tp|s3Y;>}-;_913jt?kRn=!K&M)J5a z4lke9I8)llt#ZU7K>6raMhPQ$)(aPDzz@>!yfzYAS)l6v%U0I(aseWHC+6N}z6Z+R zeoXie57D58g*3;E!v<`excLOGV~I+>EX!#M@R7vly|-Hu3Kxw(6~SwR2>$D9w6uNI ztEQmL$f)M{^QVycbbUl-9Ru0{4uflu*{SAgida)_e2@U#UgGa?`%q50%XeOae6O&! zNf)rCE`Zs-5#HLrm@a=c@G(Z$UFSeQDHjzJc;GY&G%8{q3+lfoM3!c_ZM-X zLdz(t8f?`f`tk~WJxe*9xXgJFr?4oE@PJzup`5a?AYlKq6VuQ$ZDuV)g+B*Zp*DdB zX=PKm=OT?jXHHBvEBHqYLUgv#o>u0etapY&s1oR-X{CMiz`sTLgTb#j$!8x@)-raY z?Vfi)@cfOHdOg7-q{3syBs|5~-Jel(^rX9T|0c~o?0e<$hefnKx6Z{OU@IyIvLkbW zHRu;7*d}HS>Tkuf2fR)p1D{}fC$8RRUmeLmrG0u;#{U$vlw;Q8g)D7xhjN%kytJ(< zglQIFQb=5fZ&)M4XQeoz&eZ7|ylfI6+fq*K&e1tXYHQ-YDi(L8U>?*~x9bj3ilrsq zHccpyzc4Dbzm)ewC`w*Z5@gxx{+dHHo`|<2)AaKyFzpyevM)wBi!zu_X4gGhR+s^v zJ&uypH#FxN1~}=YY!X0%-NC%(2>nxXZ>zNo)`1x zC?dsBI$R)UWd4XTy!*QXk0Qd|E0>jd@`bZw_Pod!jOrg^af)Ikl|`xJvDnK=^c*8+ zm?QCEgJJmySLbUxH??YTRvsjGg!Zx$M2Y-l&v9;#AcYLU_x5WlUxADsDemhI^Y`Bstto97hM0z_R; zH$+7Br(|32!W@|QCVM+DIaxj@HEv$cryw=%hw9O19t?@GgNQ7wVFG<%GbsV6Caq}O zcQDj~E_TuV%Fl}!61P8LzPl)U>C8i*wdu}UE0A_!C`Yo@>g8=#0%sG2cvuma67w$y zfoD&(aZohCiBYo+4w2n3B6WwXVL>*Q_}HxP%&cl42dkFKtHlc9Jy&ME%5L%)s4LtXL}1uoW^KF39$Ik^`*dJDkG_`8rX(ly@|mAQN; z|K%`84Z!dBj)xwn>loNBM@^UwgpGz=3iI4i{YFTM|4@2%X%&XFl#p`)&JE^yq5-@`H_ljrelmk2|mvlmf}K`_*gE#x%C`b;+1S z6!>&G0kaF_xG3Qm&sV{iR=@#f)F?3py6g0#1R;5sP1&j91jWAg%z5cf~qDLs5tjm%0|KoqwP-(eB8}f z(CJ!>?KPxTS*V6OTK?^qk}yw?L@kH4Nm`t1rvJ8u8%Lx%{~%NI%5Uq#o;pkZ_TjUa ze0>CtuDcB9<9ZODOV6{2E}S0zz-1JjWwy+~bFB(uIc)Qo9fC%a5Bt5_m>&`{mtxS){eGRe};rG=-C##wEUg! z;O!Z?Y)c5cO|D^F53A1~I!`04Y~nrR`QsyfT&IUYm+{*cStZS*j}`u`V4Y(OB9X=~ zDEeM|X9;>=&a+kX+C>$ZYb*}^>=v%Etq_W(%rZC?;qPQ)i7j;CuhF*e?N-2mwJTxA zc95hnwP9MzahWGHOayLq5=5q);jgSqxE#jDYNWLctDlH&y@@79DsrnNB1PAZldXsM z(F60BR^^oFs)3RFDq5==4bh4QO$(~#3>1?WdH!!o$dp6p%CxN(S1C(Veg zz~P|3_my~pRywF?SpdOv5oNe7bI9f0I~x3G;qtFcF1x?V3URN*2XBkXsh>n{KXcIO zuPlHgbJ4_T8T*-=lh-W3n;>Sby5%Z+wtTKYKSX zFU@;>Qvd#@?;I(x{_dl7UT;#13>z;L{hJg=j6Fd}Vv*dEqR;2@jxg(=fjN$Ic=Qdzc;rMdG}-Si&9S!#T( zdT+v^S$nrBf6haYYj9|r%IA`Uma8McZ4F?v_!b<@xt*BIE5~x`*{d|@$ZsV6TG~_RihQD~{eEY%7W^Y$vu@dOv z%NrnW^=q2Pf=?=bA5p}+-_&dGtCSAyV_OjetPF9Rhv^o38P~56hC|s%)ONih;e;L0 zD1f|6uS1P|{a4HCA?_&@9Fu}KzVxr8irloR`WXtc&Z=Rsm?C>WZ7|ORB z5<$kbFn++ZdM@tJ3zfYI5eoNV<8cD>(;f<*bB8a_5{A3p|8k*8quT{M*G?z}b2~iL zwx9a^Mzu|!b$TrpvqpR&-KV&1(m>_>Y`2Se7UszG)tUVtjCO+Km|qOF}}Oi~MVf-hmrHKpwhW zObxdJB!ui#5oXXbcNE2q+lSaBU9?Od7Z9qww;G1C_~Q3=-;IT~SFJn!I{Q9h1^O z37Q5NQJrVD*w?@NK=5l%rpa>ZXnH}Ak%6hFeixmPNt$?2Y-a?1F4tl}9aZDBK&QLF+r-jLK@Mb0Yo!#v90M{{S{K1=eJfBP5LNJ}mg6M7FOX z4>-#gu6#8swB@fHsjj$8@!|}Dt>SPcBW3xa>>_)IcRzW+utZJ$j8}S#fYGoMfCTfP z`CDin3Qrbf9US6y1^j?wJ4`tOUC79rr37it214N9aOuN|n-;$N2#M7w@1ZdW)YE22 z;04`Cw%uG?>rX!|T{HX7C$%t5E=W}asEc^af9%vLpL3fj^o=o_y&LhQHI zukWAx?~M{Ll05l&c|85r<_maY8XYn4A zg&NR%4RUQ5rOocwWFM)EUR@<0ew+dYn{14uJXRgvskL>4>6zV#r_s|y^8-CSwWeo+ zG%rjJK5U%VO4E>|8C-auZj)UOJ!2zfmX#?Y|)W%?2>y>a(>J_?R1T1B;A3jW&9y zY~8;F_qz1FmdDyr^2Nd55#KXC-ekj$rnuGq;hBP+r)3&fbkS#!o#2f%7iOd|mxaHH zyvM_s)EwS97q3h?@!jjyl?-Afd<EGb(+xOhFpPk9$}NzGxE6|PN~0)Y()3qW88B2UGzT~&`G?|>dQnz zD<_?ve(u0vBq6_@*Nm~|3AVy3^8^2GD|{q$oIm6t92u(v198{f@oxPWI+Y3SLX0N3 zLACC~hS_-DKv{MSc_xp5m_J<`I*S@8$o=c;}~&8Yw3QGOrH)~vAUXIZ#8;KW}W zG}DVx_}E|{xfeA53MNQJFXUy($g-gOdSJXWRQ!)OX8$iR>1>kKPrFC8G(xyI&JSNB zQjDfrpNCJ)i6W>NDf!WMF< zHF0~&7olg0`7g|{LLn!;0jd*OolX5M5s(cJRA#`;l|rz2^&j^M{Z^6qu0Pnk59NRL zi+|C=K@lH=?S9791fxw>Fg4KkaNxMUqSr~hl*uuUv@{9`ebX?)Z!O$~S+zaR9 zs9f>T>DbpuYT@wFX@c}-gepJgAH;bdX95C+Bz|Mkj z+t18eo1TGCP_fSr+9jXav(OhVnht;z5AlZzG(zkATWrHR|I;Vf)j){|JfxovWI&)L zGiGbAA|)ea-VGq26WSW28cW^%_1$s{38plbT8fn^rhK0!LJAWM&IXZt9SPVo$z4NZ zVEkfb9X$0ioO^4}%hG)xsGVU@eCc<>0@CO2=d(`^ZQUS(qi=T^`|^?8ZE;v%riGXh zm@b1OM;?N8s-r`8W<9HmUW`?kZ#HHFpB@FiYo8x?m>%r>W=Fz+wm`=UC5?>e2);bDBxgfF5|W`Z5d=} zdc>Z+g1&o@%ssq!2hhe}(T=sku<;R*{C0PMP>@+uVwnXQDYnHd0tiPIyMQoDc<5r2 zODJ>rnXn7i05EUv04hX@Y=~jokaQMPnQLYUIIJ2egM7W7O%3+5*j2a>xR?&fD$CO2_itJ*eM==gB%Hff7a!yVA*}8USDlW}SjeEhi4s&?9<@I9a7^TNa6- zM=n0U?f~qDyMOU!G1x}a^eze67!-S)OR~erq$ASyq-j(RA|{bR%gk0f^!AvZPheHU zh~W1!0@=`El}&ch=OiI4)z?Rb#e}dx{!yw-K--!QVDO#bnIHMk`ffXvel+g7HM@_6 zI^nsvNOmF$!r-HJD&6z-Ku=VmmUXBHhU^@zZn1y78@E#O=C-g^ESBQ9z|(T7a4VAo zIgA}w*shuj8$MCt+W{E8B3PF&u|29!08(e7zxxR-q5!rzSk1QFD^}3^Eb z7#Hode1_n=M!hNwFB))Q^?=+oYD9zHLuP;I5k?HwwS7u7A0sV>VMNBZ7#5U00O7Ca zVx?Hz-tHfg!Di98s|+zctlH(*bNNC*GZTsEvmu+Plj5BE1P{u*2Ro>i1ld#5Mdv;J z8MO&Qr*007Ff0Q4``_gr$?n0X9kg>ekIB(GLq2b)Gtskf*+;)uw$A^R-KiuwboRbf zWxf3;5j6~0GAkee#=?7j!Zi{7BWZ}qqwYJO>O-@UaB^=6SoAhi6hSWoT9bKL_?GCL z0R9iv>Wpcvu<*0eb@KBUa}uRBTw6asydWj!4rpRH+F2|(6h2q8#ka3H0etVyazVNd z#lC+&cgS>G{si%NuLd{t0p!?!A2y}r_q6*Rcm=qHE^dE7f#`Z3*42|h;y_ttdHdjF z(*T)-%j^P)mzznigiH(niuLrT-{cyHbuX zgwxmHcMi^s>;;~Drf9=Ua8tY~mAbQO=`BSipK;qbv?~NcQRatN<34R>XDRL|9~Rl;v1||38u-R&cwewi$+DRBnsSC<9*T|J z;O@dTinZ(Um}J|-i|v^`*b*t1E7w1XWS=S1zNICQx5rk7eUrc*D9L$3%_53LOd^Uo{JuRd*WKQoEH;urWGoG9w}BV z*R`F%^z@8Pt#78`i(ERIhVA-jSvpxiM{)@VeZJ%To7-N8D$avj2t)0^Kazc^`vJ;S zA;N0h{Ak7C9utOiY6D0xxSE7LY`$JQu%z}QEOIWJboausJ-i2dq9b8Nh4Fm;%~deu zXz@&{j*gJ#q0@P}iR{hhgi^21e6edKG-ALzeZ;l14^Eb;B1XXb_^q+=u`x(iW2s=t z_8Kdu!ggEHaytOzq=EAgzD$ulxj?mJAsTeSk$SKRC=&rfaPQAU>on!4hSORbbs4v-#_rqGJT|o zZ|eC`f0xsY2OWGp(^G*$$J_;`Cd=4)L)L!bU+w6m_UnO-D)Xy)tV_cDO=h*vyrfLW zZhEqgO)34spp2v2fzFcq#I`oOucJ}Q&nxwFo5`{UqHB?@#H(i~Ki|Sdi93FkcY!So zKQ>>|tlzNwiCDw>Y3vtGTz^7@hHosDF(Ivc^7pUyA@`ljyr0w*)-7;X4S1A^IEdKt zm~0j^psadbuA3+H)?}$lX3K0-TgP0|cy3=y8Xvgx`sEg;AA5>gwSV~C^({`CO6 zJw|<;dwF@;M3KLtG_0}THcljjv51D0VkM_ePNCn(pVhYPT4vr{mm(%1Z_HTU|5PUT38~->ouPJ?aiP z49n?y#W{o{k!GcOp=XfP*9b3vP*`pCr#`S;b8%W~Gfw?7C-;AMdcflPjkT69;5O*p zsXT1P8Ztr&lUEFWkks}1>B9K=d~}W)l^yYWw=1u>bJOGbx^S9$dIUYu6rq2bF=Xf} z7{RmLvKXCc{v!9|Y#E-C+TftxT&13qlcE~$AWpr3xu1SoE-|XsN%f|>a+3X`T3V1| z#r7tIS$ELbU7Wr91;5dr+Bxp4`BYyULo^sQXanH?;tO}fqsw0gbZF$TgJKUdQ<;1v z6mrRCZeME7P4v4do2<$hotXw(q*DcLB5Re=O<{Lf7hJqw?w%t&^LGwA zXZuJcdwjmM;AU_K3^cXr@o~uI^c8`1I~&)4!1mBk6`iiI;SH77P2QBHjN691(Wsd) z&0R}f8q|z#ajhh@XSU%&8g98) zRL~XXr8%hIE8Mfj@un*!{wWVU{c;^8?w6_anQGs62;f%3h3PGO`7Fr(7UFa^1eXnTHTlQZn^HfSIdNiW7ycKW@uP&+jpptmwt|qUs{&7F z1R^cl_gXh6iZIPN{R=P8W-<;o+8?e`Ar|&>+Wuca!(vpxn?Z2)kDMf|(;urHHQ5g; z1G&$$`DqXBQ$njt{(&OX;IZ;E7=cnMy*SBXw{INt%BAOCNgEqL=5BRn ze+UDS#)@xOnp^^4viHR^w z>(IgL{I~II1`J1}q|KLV?Oc#p$5Dd9MzDe`AQx9-Rfh_D=GS5+v``UgM%yyy_$dgZ zyeOo3#gAhS_(ykbvjAE7-BQk)MldKZ+A(7}J}~P~Q%!e-S?JaNBdH(ZBQN~ofU#VZ zIT7D910{;fl=@c#$9xMjUtk~o%q0Ku3@{|{uip}VOU>AYJOYqDcO*ZFLePVc~wX=rAD$; ziIus7oAEKxJc?lWv6)KjSFRCx7Up> zQP#}B2z?Von;BUv=6WfG4`D6UGmDkyszBXc;p|I!bUOxUjof7i5>;Xy|JdGc)b(wv#JQ6#Yml+-y7Ao%)el z*f1<@>NR2WBV%oRPX-A6MgltWX`j`yj(v+1&$W}&sSGmt4uprf%5bkb?lz9VJ{rV@ zHe>kaPub8l5kb-s7s*1XW}^IX;pS6JiNF)+5}|V;NWc9g)-3rpADkY>-CFJ7x~OM8 zMpDZ3g(6FB$2%2^;!|{lI+i2?Pkke~oGIKu+2C8)fYtTj9v$A>o1l`UlDb=2L{Zq_ zbEk(?-R-??N78HR0INtxjEIj$u9s|il6g;tNH1hYWCh-Iz5IJ%5?80FIKj>sI;Dg& z7L|!`#H0oAo7TZydBqmjxPK1&D05J`@#J{(q+i<{kJV8ZTl`%Ar0gpXXTUMsM`;{G zdHZ1Q6dA~3#p=Cy1l_*btJP;ONzU3xr%gJXNRM#E%o;=XRp^+`XIRmZxoQ{5wUb#$ z1sntWi_p10RlJl2hYalrL!d|+pKVOSPaH*HZ~H-5 z!fzYn{=;BEl3hD$G_VA~wIX~+?~k;&+H%VV?fXeZyqJmzB;ZcQ{hZ~wt@Cl**c(6s zuJS!%9*m)r6df3acvX6blIxf&?dIR}I9rdcd&}DyB>KT9mP1pa(SL^1jPQ#oP^_-OGW#HA*8}aj z1fr!6;jldAOMV{3AF2~uAo!FbULlJh*>IQ;DMDJpg)o&16ob0gjQMki3fduX;OOHe zsUpTlqZN7DPF|ARs;ejrR>y7wEomAzrEE-X$S`v;MycbiKP19VKgnh7FkJfYZdZzwfG#xd zIrCCN`1@&eI^2;BjoxQ`asrkgydfD=#0q}L<>hBxe|TxRKfJVlk@Dj^27Tq?rwxLb z`ic@H4oJ_FI`Gjf^ zRUNmY)`ojpD#?^`K;V5di^nnQfM1t9d1Tv2jLVUyrDgwrtYxQ}GVcDkQP?wUupuEu z5|->ryQw74%SCqUotCtAS%Z=rJcI{GHJ5~kLL&eMl?>E}`Dht6m7LdX#G~x)Hxm0} z&Q` zZ{@Pu7UN7U>)^e4LF@;cj1l9}^D>jP9ia2`B-y|pgRilvFJ5H)A_U`0x;AC5>Dz7N4)?RJedfjQoeqf(}z)&XPizww?y0Ry> zxDMh*vMH0^CnF-k{pa3DdG)YKJIAKmZKnoAmehqfn|b`>-6{^+2h!B2^s}c5G*hh# zM#KJd9wACg=y6Tf>AONjmJ)VUZb~iP4j9o8`)1cjELqqKC3mk(BAEBIqQu_LfqgI| zmRf$ek+^wnlKjD%F;t3!#cspeQPBRwu$ibJVu+Uz31+UhE8Cs#j55r2hI@AG`tJBx zsYW;%zFuOk*Vd$5qV3Yx%s@>?LA}vZpV|AFRB>OSWq+ka^9J;#p*wju1|2i3F~|Dn zQvHrhy=PecK{hy5)LXHQ+5%~w6(|sY2wA}_9enHz&GNf!8S_%}_AJrHt=b|-jG1s^ z=FSXp52+VszUv}G(C}+zt>8*O-F-wcRRz#(8%zr!s&x~jW34dh;3ktVLCc5_oI9T2f4}j za3p?Q1A&@!yIP1<$Foh@=p0?p(LFX_Xn_u!a$=u!k-Vso255o9gF4v+99|s{N`4&M0Yg)9dO~JDErd85LA6O$i4OSQUVHlh8B|7v{mwW8&|J zr%jU{+CbSV6dr$2`iaq1xpn9BDz#O1XCryfINMJ%o_Mx;I4aWs?muZ>e=+3_lBhnz zykU1h8v4`GS2pl8u2qhf^{tkabC!=bdER>g=D7@Sndj7_lsaa<(7e2Do_;*j5^L9! zv&W!eTYZ8sFEph&D&*)Hy65%w6ZPoS-83St@4X1&EMNX!;+w`=*ub`$2%qh3EnRLN zxgMdek8~cao*4^w<2JNCBOOVqo7!oH&e%5;cfA39eJtie#J@VTG?W7s$|5%b= zC|k>vzTz!^`T@4xP~S#3aTVWo+4UYPdm8^p}|%OwQ0JsA2?FxvW?-P{cL5 zrpYsrM6JnNrPf`0Ugb|iUHW^-=|6v?y6TD-?o2oi+VIe`tUQM($*L!y_kKUYaqQu0Mny)d=w~Yn zwFvgy{q>V^QE--TwcL)VEr!V=f{}kzHk4{ppcZRu7npNk!0p)*Z znD<%AtBly){dq~9kYnsNhMHD0rkA8ea{Ac1@KQ4L52pjXvT2Lu6(K@h->m~mk! znN^Mo`d}+1YpZStI6}f?jUoaXgX$}%O%aeAbG{f&P%gD0qw#dYyYuPgKPJ2lV2J;H z!b4T)d@fHm^*Fs0Tv8!_O(*NR-aFl90POi}H4e59^Z2fKR~MEq zOdzq=&5^@fBU&<r#^wIpjD*N=}5W-1xS)hY|PD8oqXJXk5v zdHaQ|Qr=c+fvFU_=~HOxn*VcC2YSNqj3862Bq@Kc%c9y)Vl$R;Izn716xoyU{_&RG zw=)+R$9FeoR?7?TA0pDCSheNRIul#xThjjA!{vX%p`j??px*AYaMN$s)O!)^bh5(>d>#8IOL?^Z$=|E81D?z;xV;E9@4N8C_Hn2X$%`e<9P^jCQURa}Pwb|^ zRDk>Cy8)h5QM?B%#Oa5v)hreG4q;dBzt%AcibbDbbc;bZqKTkN4j+PDv215$jZ^c+ zKrX*%=Wyq#b+60QW0oYa)N>|u)x*0apEKS;eBL#^Rv^k>{@!HHQtef(uGHO|Lut8{ zlYImbzTgEy9VrTh94=AOJp2j&-J0yTVedDHQI-OhkLy{X8!`Hy=O`KVbArmXx$Ffl zpTkEZn@^4pJes{X`-c+Sa^!<2ppttCJJ&!!Ox7xr2UoFUzE@Y__g?pr6OeFM&$(_4 zc^$7V;h{!L4IANq^=;?lhS>U5isboos1Ljo6_DOjbi$$vi$}^D5pUP0XRo2{{03}$ zP)pq+7(No30f^~na(d@t*B$u`;`wpz;+pruQJ&s8myRp~1MC@q!*1i#gb1~MzSG|h zMpK~+go@-#J!)N@56H|%COugCC(tt;)SvC2p5b$UonZt)xdhPAazqZ8YA0aw- z@KQLg_938r+t19+%P&e9Wz&I48#mfhyE#H)V^GG2=>}4Uq^tFPxsN0!kcEBQt78MN z?cr^Ar1h@6^~i1k_{Zv9s(=*~&kZNbm{L+CTpS6m88!0E1uX&60?b2lc8AYCdnw@! zOUyt-UC=?JuwddC;W-bwf~tnCHy1Lc4lKacFhA*7L7v=TYm2K!OP2hdtu(XrfrzL6 z&D9UMYQGXTwH$w9$eonNdz>0hL8p^4rY%$YNe6aEj@vIHOndw9_pWx{4wkqNav_LQ z^NAlnk}K?&W3Ayv4)PGPQQcdA2Eeii_D3PdH4juR%lz26<#Z-1SM|Wn2>g#8?AI{4 z8Fg^Z@kn|?^vKU}_4nIdTi7nJ`MT|)86Q~FEhBIc_#J4lTgb(hV1e~}jwB$H;;q3~ zZx(;_?fQcW+iaEB+tN;D*E;8L@hQp}w5*T?qZDY`~RtNB$0o_c8ayh7I40F*0Tx8yry^rb&BWhW8xdzEk2n6O~& z)4xcrcq>8j;$Sga1{R2Dx*?`1-@G~6llIo1XOid*6mxNE(ie!tT6nQ;(}d4`aBr zzZ^XLvw5cneF**MIT@?(1iI>b=+WmXKCot5tbkwR(mv4xwG4QEu@yNYF_EP^8qJgE z3ByO|m~%3BYWb5yE?T9q(x7Y}9D8J8nTlxgx~r@VNKm7GU0fA$3ons5T^LEj^a8pxo3?ek?^Ve@7eMI!pD*?pd{&Npt=n{D&A zH($TG64o%8V^?(EnS~v%;FDxyJqv-`IuFlsAwBK<%EJ=-;t9)S+Ia`nO3!0K>6@Kx zQ`;7$0>&dweuZ?-4;6a(Ysu5RKAS&TabnHO0KorJ4+E^>@ne0P&zM3+nNevC`J($ zXf#c`^m0f_xA5>*jL@Up#3y^Mld@|@KU987Y2FlOp22=J3l_!#{C9R#d>e**o$*&p z?@bv@_ln&VVD=r<$6$@q_<>s}>zP2bCYO@)Ns$0;H>WRX{`2(`GZT83g0WEUkr11; zW);HRt*K=>%9Ei~2O;&MTuORfNZ``mf9fpUQfC*LbUF5|B>Z!EUG%7rmm7Oni)B_n zgv4GmF+HsNn6I9~rmaDAw^<`)&GudD(|#DB3Y%ULP1@zfq{Wi2TbSYFNS^ohJK7n` zvn=G#@dt;$-E2Ly1AJ+cXRqmOO~OL)4`AAHl5#z}koo&y?xdVeD~lHYNte{kyOyz% zIOHw-W3&qeuLs96y(T$ntS-z+ZjgHPO9MeQv@_l2woIjsPxquD0+CY%;G_c|1eon%u|WfW3lrI+C0yk{iR7ZXQ!T$I|R=WO(R2U1Ued&(< z`9nl!voSvfE09}YSzRGW4d9=;anmx??qWoXpydONObLBU7fEiKAjcb^HL>GuXgOTI zm3sac+$dTEo=lM5G&|+mPDdThPkr%2Mhi$*AMdg5ud@F)yFxltE`v_ ztpnZbhw|yy_Esx25fxJ^N~@&o^z)b7U@QP3_G(e;dmA zIX0iLpNN$yCpmKZiySl6nfmni%xEWkU4xgfoM=#%AD&(DhJP;OMFN>}LS3O8yGpZv z71Ye3+l9#UoRQoJUOTE|-YJsVo#+u>s+Rw{%eupkFA?HZAZL4~&;3T4d~y&CnbIy+ zCGG73S$CZJd^;KxYaoV}+Bue24xBWogxXcgD0oB8iithul5wpvyC1TT$8DKMa{5T( zr&2y-3oo6YOv1`UvFiI3cG)cVh zZw=+(qr)iv zz*t8ZzllUPxCkDmMV;NCN=LaRH#`Di8V#m^b=lqHaovFZTiG${A-rnxF6MvEf&mQ* ztCHKcK*gH+nVg%o>zO{w8|PTU?x;l;${C<5f3MA#B`KxAmrxlOiB^EuqL`=hwSul^ zmce&LD`2fA-hR*~qGw%gMGO9&b=8pmGs)jdg(JAnig3iP;aiBmQw{n`cm(>|b zZ1FZZSY7e0Xxw3joN?39S}A{J0nm2vaHe6I;xfJ(NkCVrG!kB7u^i*4z;EKGiD8Rx z_hR%7SSyw}sv1x%luKb{s^=?~fq%1>3t!^w&E%S4mg@5#axPbk1%Bh_!c4fzi=D*$Jn~A5R9Y z9aR+_Z+Bn0@C3^=IefY;|2SXHcCZ`{iiKgnYGRMz^^p0V*)z$nOUaZ8J|~lDgp^~% zeX>^xV!6o!2d>hQ3OR5w|2lC0|37g5I%NOfuWFvwAcUGLDa!Iu6`_fFR#;N@BF*Ub z6Pxo6bY`R1F^56ZS!m|7T!1kS>(@#bV5y6ByO`5p4qg#Clv3+5*T!!YBKLoNXZZ?} zx&la@Pgx7A+!|71CJ$fn|(8x88SaC*LMLa7K;>B*faq)LlIm3~1N zS$iQmB8gBOa+~KN8Q$|#`@6n-?`QAdT6?YckN3CU{rvf?^*rv+HJsOZ z9>;MWr@T41Iy+6@-_?lrXdw(F*|K*ZePM7t8pqr~cV|=O&-T5DMz`i7}QJni#snQQ^xOY@2df#PPw2`ZmwTH*?wm) zJ2}6zml25<@`=SO4YjB@9nVLAzZ(MO9;4uOm$J4YxDu~%h#8mCvZCnD$TCw7+W@8F zG~rqk)rz&T+<1Jn0A|yANmy@IAS=7f%gObW%~>NzGw#lzHIkuxgfh)Nv}imEC++@@ zff|8Az>Zy-NfoaH+Tg-XnT$VA+Jiq&nkjXB=k2N>&QiXmKJhvF$Jl3Q60XNcEO2_m z5DDz}=yr`@)~DeMmq6^Yjc9_p9~m$T?lP&rMRC9mRc8KmhwK{}ik!rx{G~TK&&Sg> z`MifgtJ$~}U9xVMSxDlJmP+7kxBSbq&Bq@5-3eF6jN#L)oF~wZ`boAlc*TAiFD@yR zl(p0LKXBjUU<(`ypQCfH&l3G)iM0)5@Z?RZryuwM-0kxc8e1w+xl|pF=*NTVkav`i zK;Gdv%8%q7RNlXnFA-9QDMpM2TFdXNwSKuSP3s?kwvlW|pfzoKTLr>I2nU+z^B z>=nJbhqq+_xHpSwFiLr0w6{Om#Jn7{OW~!7PMcCzhWloY_9K=40X9mpocM;lZF=rW z1>TwvbsVK3KQM9r`-C*8*G)3XL9D#y!&GkNQ#i82FAOGv{^gMsfFmoOG;_`V7;1(> zus=1G-s};=tN6g*XvP32an}R)PiIt^Dc5_>u3!;MX@50N`>0FnrNGPQr>V=VG6G-D zw!GhlBupv&Y~`aw`QuSb%VlD`T*8NAbl!HuqFJ&=3C|Aw|Kza0`u`;C7}$*k90N1V zQoo#YifgqFGB)bG8`$K$@dfFT0Wi`*GoR)^2D3jk9mdUwH*TN|5L1r0SgtJm`7YcV z7jKod-&-O7m#turZMY*a>}Km8)8MF*HQzT?C{$dH_pjoKoEUt(SmDeyJK|YexZ(y# zLlU{2-%4Ep3iP7!_Xne3hJ!AmlURUS#$Y<&xZxT>k7KA&^f2rb-x!(br%DGPwhUS`2GGKg*+C8)ZLM@ zd?-w|j@clML(qiVTi&{*-Nxev3GhshKSa4AFHzfHVyFhIJ2$GXcSK+uB4M>}O|=Hhz3P1lnc@9*07R z?r$;JI;W{vguzP5UW)fe*|BRK}Kp zofKv_6AXqRyyxFy!aTvT`cx{r3xgGvyyf}jhNyoP@>u7XwZdwG2}l3wp5jtek*U>V zO4G9g7lQ!9e0k$&<9XND1VdAmLuk>6^c}o_>s*PHhwc9&%q~^RNsbdnJpAf;NmHN; zv@#sn_v4-;q~kWc&?0FPOK)1TL(?^Id6vw8hKzkeWpDoA=l9_^RxLA=`MWM=Qkd4D zZ%0chz>s@wnhO{qj&7#t^RZkEFU@FRhIbZmDGV)gU6w| zYSiMo6{8iK9U>jYS6D`4Qh0{Wy z#b#w|ps`opq8%Z*QY&{j%XF;$iC;CW44j&ZkS9E=fW~*|1^egOZ(Z>@NhF?oE?jCf z+^Av0>n}A#$Jod;jqjdhw-^pCXe^wD5o)x&nTQ`1_rWJlHLy_EhihuG^=Q5tIOs%k zGCa*BKLOfVFF1t7AX~gJRWd9|h`N=mhD3$rzmE!;6@x^}4q^_^!S|b8>>=q1Ba3@C z<9{Zk4$&b`I7q7$_qB6Z6OFB(zMo?z{EbyJF<56cpca$G`gWbx%dEhjG){3ADrN6B#7cm?{<)2`Eei~x#XLR-l zPCL}mewW)U94}Xg!4h5d$%-aI7_0#bn)S50=7`_n zZ#|=sopvu28;!m*Slq&KjJtMxmCi5BAlv)z=x%qwim~*afl|47SSc^%kWJfRg ziI$3Dvg^m6Ib*Ot`hM0kwg2uar$#wkF&tG{EHdkdCeEQl&E1fbQFn8&LCvpPXdMrn zzvyj8%%(}?U?bs(?;_Mi`gABG03-@=eP<7~WlQ5+<&z0VF~mgfPoy#RSur8gR<$f- zAz!Noox7$dMsawBK=e(4W1Qs6~W?cr$?cN(kx! z2wQZ_4s)0#GLGJMXdCgK&Y^VqFq0kn1!O-Iz8~|*kN62`1el9>(nqqdBoSlFg$PcY z-#Mb{3G#vPUW7$-nb}7(!E200^^p~ehpA1J$Pl!bCM=5^%xsvyo9z+{$!o>a`XYI5 zivD?vkLRg4ErE}#rH4G9f=ch-nY?wu53s22CE3w?l9k1i3!j5lAdlAnwc6O|3rR8g z!z9(y$!GZqTpzz{)i#>M%L=^`yb}~%oGnC}NnY~e^OpcIbc7o~ko{ZS>vi>9hJ`{l z5^mFDLA|`w*#j50?bCeG{h2)b#q;0iu8BPH72hfJ%_{Fmb(J~(*_RUBqd;vwB>b() z;8)X{1WYC$hBPXp*3@KZ({n0 zijbv~hnZ3I4fueGc=1=-hJ~gF@Nqjg9_~0(5Qc)j<(F^ka6A!pd+U8mOmw?r6XWQunN6IDJQ8xv8;$8_XcbdD!kp)p z(WQ5a7fFoNL$jH*6@8Uu4qPqIxb*8w`)*x(hKss1Qhj$chk^y2=qRMyaDdWe&FJ)1h`XME%NN`<)eS zdyJMuQl`zDbf^x?7{5`i``2m1J?`y4al&dA<`vilDsj)NWC}^$&Hj=v;+X$gkYZVC zXJ+a9Dzh+h$`Qs36;ot0u8cReOze&C9YSf#RX-9kGL$1q+ri*tmZOz%Ma7@S%lGDJ zNIDt&Xl73DMHi}K4o9@fpVdHsJ>jK4#h5lAw!;MFwc2BgS#eM9KD;aYx|&<%HRA|+ zv**I7HEAL~Uu|Z0o(f}_my|}&PS_w%_=S5jgh>yKhghhL*Co?)XH*=AbLWC0*c~8E zolGH+cvjO!b-gn6)jsIO4PrjsWaanMl3cFT(>qt_GZt_5ED)P_W`D+SMvgk!ZkH zu6?-V;`s6M3B$CgP*gh(gKxi8rOF6wZRE?kfCAaMbdTaQNErhz{hi`aq~nI8vq&^p zvx@wP$yxp2U97Zx>oE3u62!2xvJY+US&b zS)Ve4I)ii)MKRj!KWaayAuU3K(yXs7n6tB}eK}Lz`OT^1w*l0c*y^T@oj+Kww)k4D z|O!%hty8SAG6`p`>y$7Y{ z%OP=#E@%cXwCwj=m&DQM`6Lt0<|{ZRluxrPJAJJ6$GkIcX~SV)RL!{E%_U7BvCxy0 z{L6>dYvo)`I5PsDS2;aSofXF1&OF0#wx5lx2(>G~QXT-Vs=hQb&0a5G8)P$!DcwQL-MzS>R)m++AIuNmitrDt`DFudOfLP{t-5vJ#I|@FH z4*01Syj3c4R;XCRuEY`b=K*k8<<|#s81hZJc3>yf^*rBd{O==4fEqiYar|a0(QDT# zk0UE)W-!6B1<>%+*Th$vBp%2_y&lyf8~#*g zGi#m@^@QAK>R!Qq8~=mYZ!gQUG+6UO6BJF>O>#L%4CnhELn>-)g?EcLH#vMzOLjOA zm$9(Yqwr0d=6I&Qb4gXADJ)`w#kXto1vySnE+i^j_}-P1c)PxXcyxG!CBBF`;`Q*- zwR$Xn-WVgfuTnig&cRWGrwHO;jh#9crK`7J_|=)d#2go-OG)FsB$UYW(_;I@;^Ggb zZrAP`ixY~D@iM^^3*mkb8Hc2sa!@d$wp{g?c_rL8)KJAgbRj*r%aylbYv61nZc-7c zgVEcpt^a||@N%Qh=FJ@%po~mzUu-zZaHsv8eJ(NAWRT2&7*aE3rjhb%`nHCkKqE8@ z^-_H;qVqfdapYN9bF_-tbyK1H*HHytTi2;kmyRYyjt@4KBur)YG|F}D+2LM%aW|&R z)uHz%1{FzMug#^VfHP2*PR7d3PZjA{9{l(_Z3Jbu3Y+#a^R?j42Lrh(s{Gi~t#cs9 zTw-ZYzJy^oLvYX;N#0{2&0${ed_Xkl0&SccqfszlCk+bft}=j3IVXRKj&+2j+ZxSd zQzS|VgvOWQ43m|* z+oaYflD*YTcXl_1`T<2VT1aId@999pRx)25*ke3P*RDl22tjdJexo=v7(kVtjPm?G z-9YyIZ@fZ33Bn<~6k>0U9veuWPZ{#W;gLFM^K(IeJwW8ZEi~D^>hYd6aJU&J%Hxm> zkU+v@l3LYDSo|yF@Z-=*c@Vlsraf=2|tJ-Nr3&Y<7T*HmexR zzQ538cF~adc381Ns^Vir07B>il&k<9j|Q8e*b04Z`GCl!QR*YY+7SN zLt-P0j%Kyl5~LwB6d*dYiTSByiVbI8&F@)BXli9QBTR6Dhc)V7Jgg>?l{u3$VZsdFWO?30^g z1j(2UQ4MInL`2?0K{5pO=CIerw-ujnf8c89cDj8RkofQ)VQNIipNtV?1iXCd6N0m1 zlg^)N!I}w%qGJ9f$po8Ye|(Q@=pxiQOxsY(i&E-%(_n|pGrzWY!$Nyh_xx6scvB@9nq$xW&>%5a4Rt7>lwPMb15!%{B)#n}sOP1UtG(+*=MDR0*)-dqN++G zZH%fKC$zR2LV`iueubu-nIn2^Z?VYn980pih=Qa41WS47<<8#aYS2MGA6W+WVUZa z2l*ZxhS1sttu8kW7NUx!#OgD#4M)s|NgtM>me)$QLYRi*gE^V_!+Jum57RCcACol^ zN=X$z1;8$!EBl!9+WCYYv z;B9tM5PG(W_ZY-!X58N~5jJDHe{su|$b+!pFgsFRZ|o=_$GoWKKy7u6!* zbpm4>>CL>}$R}O(o?*oVbM>5%Hp`h2UL1v#uT~HSUt=D-jq}AHolKW1>-RLRBQS_4 zms@Hxz`sT1c1eE+OwI0Y1jq+G72T?L&#U@KXW`Lw68D+KC^uF};KDtbWQ%wXEw}EY zAw4aTNsblD7Ck|)Cbl+d40={8C05!O?MOP7b6I*?%?3JQG+xuW)n+NZkk6>>Kz?b!mSL$nk}Um))j{AHyFc(u4y;2ZuK85h6EN|n zHTqIJ8@-ria}A0+HRtw+nZsLVT{@G4(ipzlFOTvWy@p;03cm2!$&CM+O059b{3R|N zkROLtUR`C{8rg15Ox(W!v=n~APBVroFmjAvg1R2=c;ON>#15kya#dez*f(iM0c$Iq zkVjpL5#*=qo&-CEl#Ta=%}@3HKN)EVU<9PLfH91e*YhPp0X-}GYaKg*(oF%s@@-BVu5hW>P` zNd69t?xmt8AcV%7ycoxBUlYc-IBJ}Dl-^q3>(O8nB#yzxMDS!Rs5ii{(hlB*G29%%>)H`d#1(r$}A(Tk8g831f^4}J-`D{sp0FI zzK|#exXZx-&Y-f&c3NlQHW*ZKj}Echk5$1H(j|^^<;LB89lE~Ag&BdFY6=ya#Wlq5c13nuf}K5!>&*z(s~y7}B( z6eYTfGO4f=A(GxgGg_0LIz#@eS$Jr(>$^GaIY$^H=Usz&EU67G;==y*2<}Ma~gFdY;99C?|C7Qow#wVT5N=$$GP2liH(+kov=HyJ3#Oi2?{3N;hI36R9 z?a^>#Z4&>1-u#1+z}jTK%zQCGOF99_y(@6|IX4yFpg4VdWv{nM==9zDtgl&!lg3+8 zhrWg{GE9v#2_2)ucL9R+rSUqUk~X%{*R_vF>opQ<$fy111!N`NPszKk7lu#irFK2I z+al$r>MoIoTa5}_??JYo=3g@ zEu?Zh&mJe;2sniQ?{$hz;H~t0-${yCHa*NW`y3&<@Qg8?&O9Tv)5SrL%QW{|U(~3V zBAts92%B|)hIpV~x?)nWHZZNPbKm>5_+tfuH*4*{;1qB9vvqBM$u&}56g=q}C+mIy z+@?`M+8xu|XYj2t9OJ5eGV%1qA8?LFDm_v@uaK!-w5n`Uy3Hac!OkqM;@%0TAWvlQ((RIP(rAgb%U7t3Ca6ZI(gVP$Jnqjbn%7f zNod*0WwHJK$XlqnXoPS}RT!C!h-dnRdahwhbQ#|;=#iB^#)79mZGl6)%jud(?e_RI zD}h9ko^n=MM@%A2z!A1Wg2=AGWL*3U!dHAn8T;RVC`q$iO*FB4(J4H*=^<}-E|~Z( zp!)IBFotdz8q2dGi7efE$v8ohgrFw219{jssidcf^6BKy8A?9p3hrb0)+W8ZIxW*n zd(IqXq+lz|j(gq3GH-19;D?wK$fc!kUrk+L^o~_-6D5#1xY0sQ+S9kCT2^aJJZ#|0 zD3cdD$4!$|G624nrCu?0--HcghUFCF4kcVMj9#kz3$dRs$xDx?sidEXoZoXNn{L@_ z9H5I^Da(R9J0q8S?>Oc-hUEgy&ibyvl|{=9p)Qi9mW;hkifK}mSgg9Gq9bdjA6j9$ zYhAWM`Pm~y`%C8$KKH@EdD6J6Xijv%-B07(vajdw4GzJ~l!5d3TSMTYNPW&8eb@Jy z{+2cwF~@8$Zay;2XFyJA7{Wg_lOgca_cx9O#rLM8z|n|HdenT-%KuKa4SGKIoCc1$ z&S$SC%}P5Iy~}G*nW*xU<%>>w4Su6wtegpSlhCoxcdVTVk)@Uq=&&cg5|m_%)YJ!8 z6qIn;{O{xj1a%Ho`#u{Jrg@FSaHVqndt~lgX(efwqUm=gy$BySzY^82K+^2{U9sYn zuZi0MXH)!#%UPkM@dPK&Jl|VoB$cy{vA8MFVad%RD}bTQqwu|JpaRaPZw87p6ptIV zwc>;OP9$Mr4skq7oq9mvWH_Hy8EZKdVX99g4z37FKWGxGM_W9G-QF(e-+{R|-J`P| zX^f}nUp$yUR!hJ~pR=M_f@j$YB9aqKNOigQ@iaHQ7ZN{@h zvI}EcNdmIS%cpZ;Z7j>kvHAv!+J#=x`}|Oz!556&0VVz$?nbL<;`148e=DkNu3OFhHVy}*pU0n^@dU{J*i7iyeXZeO) zU${*nqwoFm?=uHCGk1Dg&$DThpIUaPf%Hq!&IA?Ck>(XQZkOUZ5bEnPdvXP`MViCt zX1{%_>oGn1Cm5wN=f+2_yE7yZVo@mqhIl2(mg$mhm;~rrV8L-hI=}%hYF;ijYpS=! zRsDV(W0ej?4cC_dAAbjpg`WDvFM?zO=O0F!uFIV`IBI!N2r{Tb-j-jiCu2^pwJ2!4 zN&ozw>bi^7XFYtaHY>E1GDjrO8P+Y@*3X`oLoJ-|CWle?aP_nVa-@Ko8wd|-{DP(LK)*xCd5aCFAJs-DsA9ds

i%5Ff5z5{J#v$bJS)IY8?-Ks9a+pPt)A_w`!S&1zt`M$R( z_BW~(9roqRje)50*mv#+(_wxoGk&#cOwt3pW)~PATxJY7SXVK;s5q-@!xl&&x;91= zK>T*(Do)KXrHP;m-((Qqgj@j+^J#4r|D9G(%b7ct3ziRz**#C?j}zvD+dxj-)jjiv zPlf~uhS_8{ug=PDwz@vSeSO#J*{LlHcMXF|yY%)xr_7@Lc!|NI8@i@gvMD)OC*uhM zh5bhN$p`tZl6c4@9CQ8Fx_^h&E#jLHS5ap>Jhi@rmVHoQ6{z2aU>1S-5+oqXR5ZI_ zF5aKR^&mW$ICsk_v7tEs>oATM?QGUlAav=^CE2rS-dzZ@Y2f-AD#$JvP0r3wv>tIz zE2ahnMYa}KFIEQDk>6_c*4Wd*k!9i_2<##+Tfg`2?Rk=2VTIF4xioH5bYwroRJHC9 zHIR-U`0gD%X$-Z}%@M3P3iA^ zvQlB*3TrWQpKtSyi~syamHBV7FPxTZ*qFO>;p1bkZ{w7d^yA_A5HD?NGE(=_`!DZjkeIem9VQ0 z`xRm!@zV%3FqVpfh{KD+i zDGmUXmXEYdw~Vg3L#4ZRs_q&ZeOh;lH>c)1RPV)GUZ?vVX$YX?ji##Z{BYyB4T2YQ z{TtO^4We0bTKZJhMc9NHZ;t!KPJTc-F~;50=G#{iE9_Q}eRDWFIEZg6tb+69PlB%SMb z?CF1Uuj&o0=_EOV+G+n4wX2vWDygLV@u^Mx;?U!t{2h_!$a$S8r%(FoT7A0j5Ha7n zJDM^j*Pqc~VsCip_b(-j=Fnp_Oq5+KQ5FD*+Yr3;6j$V9qs4#FK!3>LOQToiRN+^h zww3Ndbt=Z|5`?Fz6vH^l?N{BUTbFs?6n6MiAy@6@|(doc_MZI%DwkpE2-jY34x zt5qPmZv??3f~n`NoJvX1aGym?a)^SN2tp4W#_8J z;|A{k{#kfSafC>R8-4npMg`iqe`r*=T)rFfjtgl?@DehwmILi1&hx)wNdIA5*FSgJ zg#W#6@W1U>|Nrgx`{#oJap}K3e*bMM{eQQq^uLGf|Nb7o|Ic?W|M!sn9}bzpK<3-E zX8d7oMm!cDg|h_}!dBLzkIyGIZIqX3Do1 z^5up&>&Al(wJK;yVE>zZ$>~SWi~f=xn7QNxV-{FG8ybCX5$vGP0e`)z!ctH+zw!k) zh_~5OSq5>?El$Pq*KTnwnL{rT5LXx8256}k(c6FLcA{~t4$%x5HU7xwh&I3V9(y6+ zXf7c8SAlE+g$8SJK)2%K*alKQ`xjhKQ?qz^lW@$rSjz~YEPACYf#$9GoE<{UlpfqV zx6+Kyp9*cHvO&E!c<7RpfV$&g?~~|6^DDixt_LPu3A!twbW; z39>$8H=n;eAAOnCDFUEFz5CGSa8}X0h8-HrP1B*lTuUkcsw#Awg5|-7x17njSGiHd z6LajEsf9#W_P!K;0*KliF~%rzpMF=};-$|>h>BkG_F9Mb&H^_ms0t&2h0=$V)srEi zBVywG)u2*oH_=j7KRSvDo}m2-X*a&oY}vU-PF0sKY|nuqcy6!MW)G$v0Q+-Dr-VK@ zd(@%2GZ90kA;4=BEz`kAlUCS4yL{S}4r%xKOr#?vs&u0&>zII4edQVG~O&AcRVu1YB4Vb&wmUK(r`5;RR8+K2`VEMd0BlCknVgT zFbnYd#yZFrXOk8szk?Vi3}xJY^#M^op8vK|pL>MJDTYC%L~mf%`IKb=mcZMnj7V*T zJ7@*|t$y+Acc|u2)&YdO0i3Flh;(H4Hqx*k{cqQ^dcsX|8#HzbZEp4J?F;h-8PyEg zvl|Y7C{Q=>-tz{qS8)`XjrO~9Hi%Hv;jNU#C&rV>^2;6-9+YDfnV&4q4P`8QczRG0 zN>J+;CvfG-1nkdqAR?I}IN&^}GNj|q>X|~}%VGivf6@|If(0OQV#cxJmE zRmTke%2xSC;coTS_C}b}5|o)V3ygsd=Rgcjn|_?{$Nf#OC7yn%E;gk^2{ zk=0XD_=)Z1IR%zqQrsIIoKL^K=pc1Ox3L{Gs^PJ9CFdKtcBEDyRqHr-Kwfaq(uHQ> z1Of4`El@Y`T%J`}*)8254M1*vf0IlMAjW8xhD7fk*zCD$@sATP-aN2>4vfslmdTDZlmpg60 zA}o3dkdN~E!G5+ajuHA#2zn09$z+ci!AE*)l4;{icc~C&?PeuRmFCzSxCq?90x(gA z2;;cFn#WV^2o@oBU=ebK;l(K~4Y@lhIB6GGvmMAzz2toN(R1&u1dJ)7FkEcb(^EeR zfM$~779zo6aJh~SGe?+J`vxWZg-Y*bX;Imc`$qG=|0k>_| zKP<6+aLSSFyJzfW)g7{Dn>N}3KRnkxp5desFC0IaHx1BC7FI!2&kLY$dva<8Zf(D6 zDacROjponBf}hGSXtykZPV6;@Z6YbFjss!v3UK=NrrBgUQ68QxwRmuKneU`fcBCzz z5XRWvqgL?beHv~(A+-y~NNKaDou|fHvkX`|-DRLXL|cq`!9WDjCAQ&A!Yqi_nlL1- zaE|^g2JB~TKOv-b>=j||XD_1g&}_fYSa}VyBMVwKw!M6rvU3Ekj_>)l=YTjM;XS<$ zNP^O?Ah6M$sS`k1sI7kR2?4W^6AZJl{Sj`^nXf~4u@Zf40a3(PW{*M`o_%XBKQqWi z20U%>l4n^M6S%l2rJv3PBc`=?UjY^~K%PN87-()M9BM38N!enGh?9yl4wvk8Ypm!) zanb}YIa9adFq~~A8J&%m)cm@7#sy}@+mn{ zJ*f9tQo}*%)*PrY5RG$=W6!OcwL5^zP-_H`ZG83=94i&UocIE?j6sf4jN!U=5}0W! zMeQO(%Mcy0V2;HvO<3{4xWBIW1z7QMiO|OLui8amV2?LEfLvQom5^rtY#Zc3FVWWQ z&XS!w?VNXQ@Q;ha3hai1a2wb*P?|SFYo$^$iRW;zggi=D0jm$$S*+N9^6RN7ky(Ec zqVQf|cMW+cJ}O87?^p~Fi_+i=D_aEyT_aFc>;N@+XRBF?O&OySTGkDkXfsw;+IV^oAF>7zB%2 zU)|3-0vlrp;&p8V_=@0Z5r^fxaov_sLMo|khSj)h8~&p(Hv(ce!3*7Rf{RDYeawMh z^!)kus|D=~$YDY*{8J9&t`=bZ?R38Qd#P3z7SMouA3npH!(YLBN9+jjw)qIpq;-#JCw z9epcW{DO4Ngyi)S*1BV6&=ip7Sr-()=4=b{bS1O0xX65~C#1w7&$Y`p17<5-5` z+t%v^5^Tsj&gG0QshtU5fbP8Ch72e$>ec@oPb6ms84i9GEJ!g(G__>O`N}(4kKF2? zE98UikZjjK*$(*>$*c=3-#FfX?hETU3{T?g6<|yp);WeO0g%;}3nelDy+tx&^YxAU zCX4PUotNbv;GgW~EQ0#%56%+nRD=AQTb(^DiwJBY0yY*1H;1yO*7d9pNIlPlK0el= zj9~s1o=I=ta_pUy=W%Ux?&(}{_-ch%{jB3C%&BcMy}XI~+37w3ooJ~I3%83uLvLKYGiZD?5s3FJ!^wms$z~TtZ9YB-6i0Z3+t@V6@&Oqq)a-4#t*@LmHaR^+7;GfdTjd_2TzW8o(TV z2$b`-qV5jNtB;JYW_Osm2+wrMN~ss_P!`K#3yfMF1miG?u3x(V!W9ee2#h$Y z=h6o@@8;tG+F?AR0=hk&#PlYn215sZNrkEMTT+gZgg6I>v&XU+yoTp6x0QlWoL&`2>uea>q=tsahhtjaDJJt)XI2&`I-Ge>W(H{3t9XO((Hx(zdF z*`$3Z_!Eo_SsvmFVc`%cm&e_I)cg}?yTG$G0V`L8#EWx_6JZs9X>M^y5%vg%#>OPg zoNkz0m&_>z(mEm$X#Ah9Af6RR5_5SiEx~%6V~_Q-Afb)me#Y1?aO$8__L0i7``64& zPY6|NocLkp65V_vgwr~X<3!*yi?VZ@#M>dq6B}$F&(A~%9p(^dwKzm4dscd2`<`r* zHeTC}ImXS(t@R=Fz4%_f)t=+XO@Nr@^d~A`W9Kw#PpA@B`AfoOsizqjKV5+?)BZK5 zN)st}%~yD{Uv)elyfflB5#$CP?CN|cChm6Lzx|X>^Gs8wT3=aIsL;XnJC9IGSwk7p zPwz@T9l46mLg%PoIz6ydjL|x1477;4A@Cy;&*-{qm4nqA$b?@d5<6E^BCce%JL?LM z4P+6oSchl=b=t^L@H>3oWzsmgr1wQa@}qZ-){a#r4N9Tlt>SFy-tpCZ*G`KN&jil- z>L$_C+MIa1k3!E0{8-b*lljKdYWd10OdDt=i2z;)Yjm3ro*Ys#ME;Ozz2%INB`Eva z@;FJ`Z`DyF_;|G8>+u~wHrv;#0XrSBF9WT~+@`yxcyGiH3}(pGMjzvnDFL1~pE@OA*4wM;6!DTkB~88V6K)&LuW2Et56rb?@O3j@77@ zZ7!nyJfKBIMHZ|^%}V@c*(hJW$2qOzd+H8>NVH$V$d?_zVcr1%%~Qd%?7k!vly8?C zV%M4JiQmr(gKu`OlDmK8s@oacF_3v|ba~;rv6c@do!GWlZ$6gH=WDXYR>s7}OrL^P zE)3id2OGP&0ZJuDG$fl4y8o#Tk2>`-EPBGzb~VvmXI>Z+j_FMMkSs5ZIRI?}2bkCC zMSGh-#J?H*IjsPv+g>e*FGJ0UJ`Z9Z$V7ksbNP2RmsP^84_uSpDdd3}TlRUMXq+_q zMVH)*eqexiZDr@B%w(Ae?+ctXEqQvOSwCOgQF>r0EgaI?R8UzvGybGSqnrW4;;NpT9YVc>Ue3 z0T;XI62~rKjQW$Y4_buJ`r<$x{=D=uxRl)x8qD1M=S-bKS;6ab`gy+^{|MCrStEI+ z6|)gG(8~raIikD3A?}%tIThUBb1Xh{@U3J2@weu{7D&)>eE^fx#HVlXFKGF=XsG4O z+N<_hJaEeN2W96*omZO<>}oPB=|5+>I#622QhSICw% zQtX_(2j5!#AAhR@Y(cMLcXqW#jBtMdzXH2Mt`5{i zGD6g)Yb(9Tx90t)->L*#;4P|~C=Hg0EIxp9{9XDzboJ*&NA!2k`S2WAHxVH?&P#)_ zTu%ptZEN6QZSaLF2I|XqEQ^=>e5fh(0uvvvZ@o7)Sa;xrro(*$XGigoo3YTKLVDXL z9ln+KKmAsyYj3}*h%JY6d=sy3qTtA@W26#1Ec08Jz9V5kDKAX%M%0USgx*zwCi7*P9 Date: Wed, 27 Dec 2017 16:10:23 +0100 Subject: [PATCH 07/31] examples WIP --- examples/1_simple_runbooks/backup.py | 28 ++++++ examples/1_simple_runbooks/configure.py | 53 +++++++++++ examples/1_simple_runbooks/get_facts.py | 26 ++++++ examples/1_simple_runbooks/validate.py | 54 ++++++++++++ examples/2_simple_tooling/backup.py | 56 ++++++++++++ examples/2_simple_tooling/configure.py | 69 +++++++++++++++ examples/2_simple_tooling/get_facts.py | 50 +++++++++++ examples/2_simple_tooling/validate.py | 70 +++++++++++++++ examples/3_advanced_tooling/mate.py | 43 +++++++++ examples/3_advanced_tooling/tasks/__init__.py | 0 examples/3_advanced_tooling/tasks/backup.py | 37 ++++++++ .../3_advanced_tooling/tasks/configure.py | 77 ++++++++++++++++ .../3_advanced_tooling/tasks/get_facts.py | 21 +++++ examples/3_advanced_tooling/tasks/validate.py | 49 +++++++++++ examples/configure.py | 82 ------------------ examples/extra_data/leaf00.cmh/l3.yaml | 16 ++++ examples/extra_data/leaf01.cmh/l3.yaml | 16 ++++ examples/extra_data/spine00.cmh/l3.yaml | 16 ++++ examples/extra_data/spine01.cmh/l3.yaml | 16 ++++ .../extra_data/switch00.bma/interfaces.yaml | 7 -- .../extra_data/switch00.cmh/interfaces.yaml | 7 -- .../extra_data/switch01.bma/interfaces.yaml | 7 -- .../extra_data/switch01.cmh/interfaces.yaml | 7 -- examples/get_facts_grouping.py | 42 --------- examples/get_facts_simple.py | 34 -------- examples/groups.yaml | 4 + examples/hosts.yaml | 2 + examples/network_diagram.graffle | Bin 2241 -> 2671 bytes examples/templates/{base => }/eos/base.j2 | 0 examples/templates/eos/interfaces.j2 | 8 ++ examples/templates/eos/leaf.j2 | 4 + examples/templates/eos/routing.j2 | 12 +++ examples/templates/eos/spine.j2 | 0 .../templates/interfaces/eos/interfaces.j2 | 6 -- .../templates/interfaces/junos/interfaces.j2 | 9 -- examples/templates/{base => }/junos/base.j2 | 0 examples/templates/junos/interfaces.j2 | 15 ++++ examples/templates/junos/leaf.j2 | 8 ++ examples/templates/junos/routing.j2 | 30 +++++++ examples/templates/junos/spine.j2 | 0 .../plugins/tasks/data/test_data/simple.json | 2 +- 41 files changed, 781 insertions(+), 202 deletions(-) create mode 100755 examples/1_simple_runbooks/backup.py create mode 100755 examples/1_simple_runbooks/configure.py create mode 100755 examples/1_simple_runbooks/get_facts.py create mode 100755 examples/1_simple_runbooks/validate.py create mode 100755 examples/2_simple_tooling/backup.py create mode 100755 examples/2_simple_tooling/configure.py create mode 100755 examples/2_simple_tooling/get_facts.py create mode 100755 examples/2_simple_tooling/validate.py create mode 100755 examples/3_advanced_tooling/mate.py create mode 100644 examples/3_advanced_tooling/tasks/__init__.py create mode 100755 examples/3_advanced_tooling/tasks/backup.py create mode 100755 examples/3_advanced_tooling/tasks/configure.py create mode 100755 examples/3_advanced_tooling/tasks/get_facts.py create mode 100755 examples/3_advanced_tooling/tasks/validate.py delete mode 100644 examples/configure.py create mode 100644 examples/extra_data/leaf00.cmh/l3.yaml create mode 100644 examples/extra_data/leaf01.cmh/l3.yaml create mode 100644 examples/extra_data/spine00.cmh/l3.yaml create mode 100644 examples/extra_data/spine01.cmh/l3.yaml delete mode 100644 examples/extra_data/switch00.bma/interfaces.yaml delete mode 100644 examples/extra_data/switch00.cmh/interfaces.yaml delete mode 100644 examples/extra_data/switch01.bma/interfaces.yaml delete mode 100644 examples/extra_data/switch01.cmh/interfaces.yaml delete mode 100644 examples/get_facts_grouping.py delete mode 100644 examples/get_facts_simple.py rename examples/templates/{base => }/eos/base.j2 (100%) create mode 100644 examples/templates/eos/interfaces.j2 create mode 100644 examples/templates/eos/leaf.j2 create mode 100644 examples/templates/eos/routing.j2 create mode 100644 examples/templates/eos/spine.j2 delete mode 100644 examples/templates/interfaces/eos/interfaces.j2 delete mode 100644 examples/templates/interfaces/junos/interfaces.j2 rename examples/templates/{base => }/junos/base.j2 (100%) create mode 100644 examples/templates/junos/interfaces.j2 create mode 100644 examples/templates/junos/leaf.j2 create mode 100644 examples/templates/junos/routing.j2 create mode 100644 examples/templates/junos/spine.j2 diff --git a/examples/1_simple_runbooks/backup.py b/examples/1_simple_runbooks/backup.py new file mode 100755 index 00000000..84f247f1 --- /dev/null +++ b/examples/1_simple_runbooks/backup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +from brigade.easy import easy_brigade +from brigade.plugins.tasks import files, networking, text + + +def backup(task): + result = task.run(networking.napalm_get, + getters="config") + + return task.run(files.write, + content=result.result["config"]["running"], + filename="./backups/{}".format(task.host)) + + +brigade = easy_brigade( + hosts="../hosts.yaml", groups="../groups.yaml", + dry_run=False, + raise_on_error=False, +) + +# select which devices we want to work with +filtered = brigade.filter(type="network_device", site="cmh") +results = filtered.run(backup) + +# Let's print the result on screen +filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) diff --git a/examples/1_simple_runbooks/configure.py b/examples/1_simple_runbooks/configure.py new file mode 100755 index 00000000..9f44d08a --- /dev/null +++ b/examples/1_simple_runbooks/configure.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import data, networking, text + + +def configure(task): + r = task.run(text.template_file, + template="base.j2", + path="../templates/{brigade_nos}") + task.host["config"] = r.result + + r = task.run(data.load_yaml, + file="../extra_data/{host}/l3.yaml") + task.host["l3"] = r.result + + r = task.run(text.template_file, + template="interfaces.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="routing.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="{role}.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + return task.run(networking.napalm_configure, + replace=False, + configuration=task.host["config"]) + + +brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=False, + config=Config(raise_on_error=False), +) + +filtered = brigade.filter(type="network_device", site="cmh") + +results = filtered.run(task=configure) + +filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) diff --git a/examples/1_simple_runbooks/get_facts.py b/examples/1_simple_runbooks/get_facts.py new file mode 100755 index 00000000..5defeda5 --- /dev/null +++ b/examples/1_simple_runbooks/get_facts.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +""" +This is a very simple scripts to get facts and print them on the screen. +""" +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import networking, text + +brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=True, + config=Config(raise_on_error=False), +) + +# select which devices we want to work with +filtered = brigade.filter(type="network_device", site="cmh") + +# we are going to gather "interfaces" and "facts" information with napalm +results = filtered.run(networking.napalm_get, + getters=["interfaces", "facts"]) + +# Let's print the result on screen +filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) diff --git a/examples/1_simple_runbooks/validate.py b/examples/1_simple_runbooks/validate.py new file mode 100755 index 00000000..6ef85baf --- /dev/null +++ b/examples/1_simple_runbooks/validate.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +from brigade.core import Brigade +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import data, networking, text + + +def validate(task): + task.host["config"] = "" + + r = task.run(name="read data", + task=data.load_yaml, + file="../extra_data/{host}/l3.yaml") + + validation_rules = [{ + 'get_bgp_neighbors': { + 'global': { + 'peers': { + '_mode': 'strict', + } + } + } + }] + peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] + for session in r.result['sessions']: + peers[session['ipv4']] = {'is_up': True} + + return task.run(name="validating data", + task=networking.napalm_validate, + validation_source=validation_rules) + + +def print_compliance(task, results): + task.run(name="print result", + task=text.print_result, + data=results[task.host.name], + failed=not results[task.host.name].result['complies'], + ) + + +brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=False, +) + +filtered = brigade.filter(type="network_device", site="cmh") + +results = filtered.run(task=validate) + +filtered.run(print_compliance, + results=results, + num_workers=1) diff --git a/examples/2_simple_tooling/backup.py b/examples/2_simple_tooling/backup.py new file mode 100755 index 00000000..bf68c168 --- /dev/null +++ b/examples/2_simple_tooling/backup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +""" +This is a simple example where we use click and brigade to build a simple CLI tool to retrieve +hosts information. + +The main difference with get_facts_simple.py is that instead of calling a plugin directly +we wrap it in a function. It is not very useful or necessary here but illustrates how +tasks can be grouped. +""" +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import files, networking, text + +import click + + +def backup(task, path): + result = task.run(networking.napalm_get, + getters="config") + + return task.run(files.write, + content=result.result["config"]["running"], + filename="{}/{}".format(path, task.host)) + + +@click.command() +@click.option('--filter', '-f', multiple=True) +@click.option('--path', '-p', default=".") +def main(filter, path): + """ + Backups running configuration of devices into a file + """ + brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=False, + config=Config(raise_on_error=False), + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + filtered = brigade.filter(**filter_dict) # let's filter the devices + results = filtered.run(backup, num_workers=20, path=path) + + # Let's print the result on screen + filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) + + +if __name__ == "__main__": + main() diff --git a/examples/2_simple_tooling/configure.py b/examples/2_simple_tooling/configure.py new file mode 100755 index 00000000..f0a16632 --- /dev/null +++ b/examples/2_simple_tooling/configure.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import data, networking, text + +import click + + +def configure(task): + r = task.run(text.template_file, + template="base.j2", + path="../templates/{brigade_nos}") + task.host["config"] = r.result + + r = task.run(data.load_yaml, + file="../extra_data/{host}/l3.yaml") + task.host["l3"] = r.result + + r = task.run(text.template_file, + template="interfaces.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="routing.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="{role}.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + return task.run(networking.napalm_configure, + replace=False, + configuration=task.host["config"]) + + +@click.command() +@click.option('--filter', '-f', multiple=True) +@click.option('--commit/--no-commit', '-c', default=False) +def main(filter, commit): + brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=not commit, + config=Config(raise_on_error=False), + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + filtered = brigade.filter(**filter_dict) # let's filter the devices + + results = filtered.run(task=configure) + + filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) + + +if __name__ == "__main__": + main() diff --git a/examples/2_simple_tooling/get_facts.py b/examples/2_simple_tooling/get_facts.py new file mode 100755 index 00000000..b8629905 --- /dev/null +++ b/examples/2_simple_tooling/get_facts.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +""" +This is a simple example where we use click and brigade to build a simple CLI tool to retrieve +hosts information. + +The main difference with get_facts_simple.py is that instead of calling a plugin directly +we wrap it in a function. It is not very useful or necessary here but illustrates how +tasks can be grouped. +""" +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import networking, text + +import click + + +@click.command() +@click.option('--filter', '-f', multiple=True, + help="k=v pairs to filter the devices") +@click.option('--get', '-g', multiple=True, + help="getters you want to use") +def main(filter, get): + """ + Retrieve information from network devices using napalm + """ + brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=True, + config=Config(raise_on_error=False), + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + filtered = brigade.filter(**filter_dict) # let's filter the devices + results = filtered.run(networking.napalm_get, + getters=get) + + # Let's print the result on screen + filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) + + +if __name__ == "__main__": + main() diff --git a/examples/2_simple_tooling/validate.py b/examples/2_simple_tooling/validate.py new file mode 100755 index 00000000..a64a928f --- /dev/null +++ b/examples/2_simple_tooling/validate.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +from brigade.core import Brigade +from brigade.plugins.inventory.simple import SimpleInventory +from brigade.plugins.tasks import data, networking, text + +import click + + +def validate(task): + task.host["config"] = "" + + r = task.run(name="read data", + task=data.load_yaml, + file="../extra_data/{host}/l3.yaml") + + validation_rules = [{ + 'get_bgp_neighbors': { + 'global': { + 'peers': { + '_mode': 'strict', + } + } + } + }] + peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] + for session in r.result['sessions']: + peers[session['ipv4']] = {'is_up': True} + + return task.run(name="validating data", + task=networking.napalm_validate, + validation_source=validation_rules) + + +def print_compliance(task, results): + task.run(name="print result", + task=text.print_result, + data=results[task.host.name], + failed=not results[task.host.name].result['complies'], + ) + + +@click.command() +@click.option('--filter', '-f', multiple=True) +@click.option('--commit/--no-commit', '-c', default=False) +def main(filter, commit): + brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=False, + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + filtered = brigade.filter(**filter_dict) # let's filter the devices + + results = filtered.run(task=validate) + + filtered.run(print_compliance, + results=results, + num_workers=1) + + +if __name__ == "__main__": + main() diff --git a/examples/3_advanced_tooling/mate.py b/examples/3_advanced_tooling/mate.py new file mode 100755 index 00000000..e78acd91 --- /dev/null +++ b/examples/3_advanced_tooling/mate.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" + +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory + +import click + +from tasks import backup, configure, get_facts, validate + + +@click.group() +@click.option('--filter', '-f', multiple=True) +@click.option('--commit/--no-commit', '-c', default=False) +@click.pass_context +def run(ctx, filter, commit): + brigade = Brigade( + inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), + dry_run=not commit, + config=Config(raise_on_error=False), + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + filtered = brigade.filter(**filter_dict) # let's filter the devices + ctx.obj["filtered"] = filtered + + +run.add_command(backup.backup) +run.add_command(configure.configure) +run.add_command(get_facts.get) +run.add_command(validate.validate) + + +if __name__ == "__main__": + run(obj={}) diff --git a/examples/3_advanced_tooling/tasks/__init__.py b/examples/3_advanced_tooling/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/3_advanced_tooling/tasks/backup.py b/examples/3_advanced_tooling/tasks/backup.py new file mode 100755 index 00000000..67262c38 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/backup.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +""" +This is a simple example where we use click and brigade to build a simple CLI tool to retrieve +hosts information. + +The main difference with get_facts_simple.py is that instead of calling a plugin directly +we wrap it in a function. It is not very useful or necessary here but illustrates how +tasks can be grouped. +""" +from brigade.plugins import functions +from brigade.plugins.tasks import files, networking, text + +import click + + +def backup_task(task, path): + result = task.run(networking.napalm_get, + getters="config") + + return task.run(files.write, + content=result.result["config"]["running"], + filename="{}/{}".format(path, task.host)) + + +@click.command() +@click.option('--backup-path', default=".") +@click.pass_context +def backup(ctx, backup_path, **kwargs): + functions.text.print_title("Backing up configurations") + filtered = ctx.obj["filtered"] + results = filtered.run(backup_task, + path=backup_path) + + # Let's print the result on screen + return filtered.run(text.print_result, + num_workers=1, + data=results) diff --git a/examples/3_advanced_tooling/tasks/configure.py b/examples/3_advanced_tooling/tasks/configure.py new file mode 100755 index 00000000..6ef6c512 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/configure.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +import time + +from brigade.plugins import functions +from brigade.plugins.tasks import data, networking, text + +import click + +from . import backup as backup_ +from . import validate as validate_ + + +def configure_task(task): + r = task.run(text.template_file, + template="base.j2", + path="../templates/{brigade_nos}") + task.host["config"] = r.result + + r = task.run(data.load_yaml, + file="../extra_data/{host}/l3.yaml") + task.host["l3"] = r.result + + r = task.run(text.template_file, + template="interfaces.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="routing.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + r = task.run(text.template_file, + template="{role}.j2", + path="../templates/{brigade_nos}") + task.host["config"] += r.result + + return task.run(networking.napalm_configure, + replace=False, + configuration=task.host["config"]) + + +@click.command() +@click.option("--validate/--no-validate", default=False) +@click.option("--rollback/--no-rollback", default=False) +@click.option("--backup/--no-backup", default=False) +@click.option('--backup-path', default=".") +@click.pass_context +def configure(ctx, validate, backup, backup_path, rollback): + filtered = ctx.obj["filtered"] + + if backup: + backup_.backup.invoke(ctx) + + functions.text.print_title("Configure Network") + results = filtered.run(task=configure_task) + + filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) + + if validate: + time.sleep(10) + r = validate_.validate.invoke(ctx) + + if r.failed and rollback: + functions.text.print_title("Rolling back configuration!!!") + r = filtered.run(networking.napalm_configure, + replace=True, + filename=backup_path + "/{host}") + filtered.run(text.print_result, + num_workers=1, + data=r) + import pdb; pdb.set_trace() # noqa diff --git a/examples/3_advanced_tooling/tasks/get_facts.py b/examples/3_advanced_tooling/tasks/get_facts.py new file mode 100755 index 00000000..e31dcdd9 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/get_facts.py @@ -0,0 +1,21 @@ +from brigade.plugins.tasks import networking, text + +import click + + +@click.command() +@click.option('--get', '-g', multiple=True, + help="getters you want to use") +@click.pass_context +def get(ctx, get): + """ + Retrieve information from network devices using napalm + """ + filtered = ctx.obj["filtered"] + results = filtered.run(networking.napalm_get, + getters=get) + + # Let's print the result on screen + filtered.run(text.print_result, + num_workers=1, # we are printing on screen so we want to do this synchronously + data=results) diff --git a/examples/3_advanced_tooling/tasks/validate.py b/examples/3_advanced_tooling/tasks/validate.py new file mode 100755 index 00000000..a22bf30b --- /dev/null +++ b/examples/3_advanced_tooling/tasks/validate.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +In this example we write a CLI tool with brigade and click to deploy configuration. +""" +from brigade.plugins import functions +from brigade.plugins.tasks import data, networking, text + +import click + + +def validate_task(task): + task.host["config"] = "" + + r = task.run(name="read data", + task=data.load_yaml, + file="../extra_data/{host}/l3.yaml") + + validation_rules = [{ + 'get_bgp_neighbors': { + 'global': { + 'peers': { + '_mode': 'strict', + } + } + } + }] + peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] + for session in r.result['sessions']: + peers[session['ipv4']] = {'is_up': True} + + return task.run(name="validating data", + task=networking.napalm_validate, + validation_source=validation_rules) + + +@click.command() +@click.pass_context +def validate(ctx, **kwargs): + functions.text.print_title("Make sure BGP sessions are UP") + filtered = ctx.obj["filtered"] + + results = filtered.run(task=validate_task) + + filtered.run(name="print validate result", + num_workers=1, + task=text.print_result, + data=results) + + return results diff --git a/examples/configure.py b/examples/configure.py deleted file mode 100644 index f88334cf..00000000 --- a/examples/configure.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -In this example we write a CLI tool with brigade and click to deploy configuration. -""" -import logging - -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import data, networking, text - -import click - - -def base_config(task): - """ - 1. logs all the facts, even the ones inherited from groups - 2. Creates a placeholder for device configuration - 3. Initializes some basic configuration - """ - logging.info({task.host.name: task.host.items()}) - - task.host["config"] = "" - - r = text.template_file(task=task, - template="base.j2", - path="templates/base/{nos}") - task.host["config"] += r.result - - -def configure_interfaces(task): - """ - 1. Load interface data from an external yaml file - 2. Creates interface configuration - """ - r = data.load_yaml(task=task, - file="extra_data/{host}/interfaces.yaml") - task.host["interfaces"] = r.result - - r = text.template_file(task=task, - template="interfaces.j2", - path="templates/interfaces/{nos}") - task.host["config"] += r.result - - -def deploy_config(task): - """ - 1. Load configuration into the device - 2. Prints diff - """ - r = networking.napalm_configure(task=task, - replace=False, - configuration=task.host["config"]) - - click.secho("--- {} ({})".format(task.host, r.changed), fg="blue", bold=True) - click.secho(r.diff, fg='yellow') - click.echo() - - -@click.command() -@click.option('--commit/--no-commit', default=False) -@click.option('--debug/--no-debug', default=False) -@click.argument('site') -@click.argument('role') -def deploy(commit, debug, site, role): - logging.basicConfig( - filename="log", - level=logging.DEBUG if debug else logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - ) - - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=not commit, - ) - - filtered = brigade.filter(site=site, role=role) - filtered.run(task=base_config) - filtered.run(task=configure_interfaces) - filtered.run(task=deploy_config) - - -if __name__ == "__main__": - deploy() diff --git a/examples/extra_data/leaf00.cmh/l3.yaml b/examples/extra_data/leaf00.cmh/l3.yaml new file mode 100644 index 00000000..f5fb9854 --- /dev/null +++ b/examples/extra_data/leaf00.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +interfaces: + Ethernet1: + connects_to: spine00.cmh + ipv4: 10.0.0.1/31 + enabled: false + Ethernet2: + connects_to: spine01.cmh + ipv4: 10.0.1.1/31 + enabled: true + +sessions: + - ipv4: 10.0.0.0 + peer_as: 65000 + - ipv4: 10.0.1.0 + peer_as: 65000 diff --git a/examples/extra_data/leaf01.cmh/l3.yaml b/examples/extra_data/leaf01.cmh/l3.yaml new file mode 100644 index 00000000..f3fb1c5c --- /dev/null +++ b/examples/extra_data/leaf01.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +interfaces: + ge-0/0/1: + connects_to: spine00.cmh + ipv4: 10.0.0.3/31 + enabled: true + ge-0/0/2: + connects_to: spine01.cmh + ipv4: 10.0.1.3/31 + enabled: true + +sessions: + - ipv4: 10.0.0.2 + peer_as: 65000 + - ipv4: 10.0.1.2 + peer_as: 65000 diff --git a/examples/extra_data/spine00.cmh/l3.yaml b/examples/extra_data/spine00.cmh/l3.yaml new file mode 100644 index 00000000..19c58537 --- /dev/null +++ b/examples/extra_data/spine00.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +interfaces: + Ethernet1: + connects_to: leaf00.cmh + ipv4: 10.0.0.0/31 + enabled: true + Ethernet2: + connects_to: leaf01.cmh + ipv4: 10.0.0.2/31 + enabled: true + +sessions: + - ipv4: 10.0.0.1 + peer_as: 65100 + - ipv4: 10.0.0.3 + peer_as: 65101 diff --git a/examples/extra_data/spine01.cmh/l3.yaml b/examples/extra_data/spine01.cmh/l3.yaml new file mode 100644 index 00000000..7bdd33cc --- /dev/null +++ b/examples/extra_data/spine01.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +interfaces: + ge-0/0/1: + connects_to: leaf00.cmh + ipv4: 10.0.1.0/31 + enabled: true + ge-0/0/2: + connects_to: leaf01.cmh + ipv4: 10.0.1.2/31 + enabled: true + +sessions: + - ipv4: 10.0.1.1 + peer_as: 65100 + - ipv4: 10.0.1.3 + peer_as: 65101 diff --git a/examples/extra_data/switch00.bma/interfaces.yaml b/examples/extra_data/switch00.bma/interfaces.yaml deleted file mode 100644 index 64d0d9b6..00000000 --- a/examples/extra_data/switch00.bma/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -Ethernet1: - description: "An Interface in bma" - enabled: true -Ethernet2: - description: "Another interface in bma" - enabled: false diff --git a/examples/extra_data/switch00.cmh/interfaces.yaml b/examples/extra_data/switch00.cmh/interfaces.yaml deleted file mode 100644 index 4f0c5e0b..00000000 --- a/examples/extra_data/switch00.cmh/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -Ethernet1: - description: "An Interface in cmh" - enabled: true -Ethernet2: - description: "Another interface in cmh" - enabled: false diff --git a/examples/extra_data/switch01.bma/interfaces.yaml b/examples/extra_data/switch01.bma/interfaces.yaml deleted file mode 100644 index db67fca1..00000000 --- a/examples/extra_data/switch01.bma/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -ge-0/0/1: - description: "An Interface in bma" - enabled: true -ge-0/0/2: - description: "Another interface in bma" - enabled: false diff --git a/examples/extra_data/switch01.cmh/interfaces.yaml b/examples/extra_data/switch01.cmh/interfaces.yaml deleted file mode 100644 index e847b411..00000000 --- a/examples/extra_data/switch01.cmh/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -ge-0/0/1: - description: "An Interface in cmh" - enabled: true -ge-0/0/2: - description: "Another interface in cmh" - enabled: false diff --git a/examples/get_facts_grouping.py b/examples/get_facts_grouping.py deleted file mode 100644 index f016dd96..00000000 --- a/examples/get_facts_grouping.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. - -The main difference with get_facts_simple.py is that instead of calling a plugin directly -we wrap it in a function. It is not very useful or necessary here but illustrates how -tasks can be grouped. -""" -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import networking - -import click - - -def get_facts(task, facts): - return networking.napalm_get_facts(task, facts) - - -@click.command() -@click.argument('site') -@click.argument('role') -@click.argument('facts') -def main(site, role, facts): - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=True, - ) - - filtered = brigade.filter(site=site, role=role) - result = filtered.run(task=get_facts, - facts=facts) - - for host, r in result.items(): - print(host) - print("============") - print(r.result) - print() - - -if __name__ == "__main__": - main() diff --git a/examples/get_facts_simple.py b/examples/get_facts_simple.py deleted file mode 100644 index 5cbc1fbd..00000000 --- a/examples/get_facts_simple.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. -""" -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import networking - -import click - - -@click.command() -@click.argument('site') -@click.argument('role') -@click.argument('facts') -def main(site, role, facts): - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=True, - ) - - filtered = brigade.filter(site=site, role=role) - result = filtered.run(task=networking.napalm_get_facts, - facts=facts) - - for host, r in result.items(): - print(host) - print("============") - print(r.result) - print() - - -if __name__ == "__main__": - main() diff --git a/examples/groups.yaml b/examples/groups.yaml index 0bfa38c7..df5b6ba5 100644 --- a/examples/groups.yaml +++ b/examples/groups.yaml @@ -8,3 +8,7 @@ bma: cmh: group: all + asn: 65000 + vlans: + 100: frontend + 200: backend diff --git a/examples/hosts.yaml b/examples/hosts.yaml index c78cdb80..15ccd489 100644 --- a/examples/hosts.yaml +++ b/examples/hosts.yaml @@ -45,6 +45,7 @@ leaf00.cmh: group: cmh brigade_nos: eos type: network_device + asn: 65100 leaf01.cmh: brigade_host: 127.0.0.1 @@ -56,6 +57,7 @@ leaf01.cmh: group: cmh brigade_nos: junos type: network_device + asn: 65101 host1.bma: site: bma diff --git a/examples/network_diagram.graffle b/examples/network_diagram.graffle index 2711ab7a46b091920b996549a197737a84559376..871fb479332e1970f173a8bfdc1b1c8a05586a2e 100644 GIT binary patch literal 2671 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 Date: Wed, 27 Dec 2017 16:26:02 +0100 Subject: [PATCH 08/31] improvements to napalm_configure --- .../plugins/tasks/networking/napalm_configure.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/brigade/plugins/tasks/networking/napalm_configure.py b/brigade/plugins/tasks/networking/napalm_configure.py index 895d06b4..663c8404 100644 --- a/brigade/plugins/tasks/networking/napalm_configure.py +++ b/brigade/plugins/tasks/networking/napalm_configure.py @@ -1,7 +1,8 @@ +from brigade.core.helpers import format_string from brigade.core.task import Result -def napalm_configure(task, configuration, replace=False): +def napalm_configure(task, filename=None, configuration=None, replace=False): """ Loads configuration into a network devices using napalm @@ -15,15 +16,16 @@ def napalm_configure(task, configuration, replace=False): * diff (``string``): change in the system """ device = task.host.get_connection("napalm") + filename = format_string(filename, task, **task.host) if filename is not None else None if replace: - device.load_replace_candidate(config=configuration) + device.load_replace_candidate(filename=filename, config=configuration) else: - device.load_merge_candidate(config=configuration) + device.load_merge_candidate(filename=filename, config=configuration) diff = device.compare_config() - if task.dry_run: - device.discard_config() - else: + if not task.dry_run and diff: device.commit_config() + else: + device.discard_config() return Result(host=task.host, diff=diff, changed=len(diff) > 0) From 31846323a48c1b184e7a04792fbe368665f91399 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 27 Dec 2017 16:27:17 +0100 Subject: [PATCH 09/31] unnecessary --- brigade/core/inventory.py | 4 ---- brigade/core/task.py | 3 --- brigade/plugins/tasks/commands/command.py | 5 ----- brigade/plugins/tasks/commands/remote_command.py | 5 ----- brigade/plugins/tasks/files/sftp.py | 4 ---- 5 files changed, 21 deletions(-) diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index a0481615..adb5186e 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -1,12 +1,8 @@ import getpass -import logging from brigade.core import helpers -logger = logging.getLogger("brigade") - - class Host(object): """ Represents a host. diff --git a/brigade/core/task.py b/brigade/core/task.py index 72a1e694..34006e85 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -1,10 +1,7 @@ -import logging from builtins import super from brigade.core.exceptions import BrigadeExecutionError -logger = logging.getLogger("brigade") - class Task(object): """ diff --git a/brigade/plugins/tasks/commands/command.py b/brigade/plugins/tasks/commands/command.py index 3efc587a..718bed4e 100644 --- a/brigade/plugins/tasks/commands/command.py +++ b/brigade/plugins/tasks/commands/command.py @@ -1,4 +1,3 @@ -import logging import shlex import subprocess @@ -8,9 +7,6 @@ from brigade.core.task import Result -logger = logging.getLogger("brigade") - - def command(task, command): """ Executes a command locally @@ -27,7 +23,6 @@ def command(task, command): :obj:`brigade.core.exceptions.CommandError`: when there is a command error """ command = format_string(command, task, **task.host) - logger.debug("{}:local_cmd:{}".format(task.host, command)) cmd = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/brigade/plugins/tasks/commands/remote_command.py b/brigade/plugins/tasks/commands/remote_command.py index f7330ae6..47a49a53 100644 --- a/brigade/plugins/tasks/commands/remote_command.py +++ b/brigade/plugins/tasks/commands/remote_command.py @@ -1,12 +1,7 @@ -import logging - from brigade.core.exceptions import CommandError from brigade.core.task import Result -logger = logging.getLogger("brigade") - - def remote_command(task, command): """ Executes a command locally diff --git a/brigade/plugins/tasks/files/sftp.py b/brigade/plugins/tasks/files/sftp.py index c010c5e1..4a51f39d 100644 --- a/brigade/plugins/tasks/files/sftp.py +++ b/brigade/plugins/tasks/files/sftp.py @@ -1,5 +1,4 @@ import hashlib -import logging import os import stat @@ -13,9 +12,6 @@ from scp import SCPClient -logger = logging.getLogger("brigade") - - def get_src_hash(filename): sha1sum = hashlib.sha1() From 027a4407df13553cfac2cf18e0f719d020d6dd77 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 27 Dec 2017 16:28:39 +0100 Subject: [PATCH 10/31] improvements to logging --- brigade/core/__init__.py | 67 ++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 92e82c4c..8583c77b 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -1,4 +1,5 @@ import logging +import logging.config import sys import traceback from multiprocessing.dummy import Pool @@ -33,9 +34,6 @@ def _unpickle_method(func_name, obj, cls): copy_reg.pickle(types.MethodType, _pickle_method, _unpickle_method) -logger = logging.getLogger("brigade") - - class Brigade(object): """ This is the main object to work with. It contains the inventory and it serves @@ -57,7 +55,9 @@ class Brigade(object): """ def __init__(self, inventory, dry_run, config=None, config_file=None, - available_connections=None): + available_connections=None, logger=None): + self.logger = logger or logging.getLogger("brigade") + self.inventory = inventory self.inventory.brigade = self @@ -67,18 +67,46 @@ def __init__(self, inventory, dry_run, config=None, config_file=None, else: self.config = config or Config() - format = "\033[31m%(asctime)s - %(name)s - %(levelname)s" - format += " - %(funcName)20s() - %(message)s\033[0m" - logging.basicConfig( - level=logging.ERROR, - format=format, - filename="brigade.log", - ) + self.configure_logging() + if available_connections is not None: self.available_connections = available_connections else: self.available_connections = connections.available_connections + def configure_logging(self): + format = "%(asctime)s - %(name)s - %(levelname)s" + format += " - %(funcName)10s() - %(message)s" + logging.config.dictConfig({ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": {"format": format} + }, + "handlers": { + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "brigade.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + }, + "loggers": { + "brigade": { + "level": "INFO", + "handlers": ["info_file_handler"], + "propagate": "no" + }, + }, + "root": { + "level": "ERROR", + "handlers": ["info_file_handler"] + } + }) + def filter(self, **kwargs): """ See :py:meth:`brigade.core.inventory.Inventory.filter` @@ -95,13 +123,14 @@ def _run_serial(self, task, dry_run, **kwargs): result = AggregatedResult() for host in self.inventory.hosts.values(): try: - logger.debug("{}: running task {}".format(host.name, t)) + self.logger.info("{}: {}: running task".format(host.name, t.name)) r = t._start(host=host, brigade=self, dry_run=dry_run) result[host.name] = r except Exception as e: - logger.error("{}: {}".format(host, e)) - result.failed_hosts[host.name] = e - result.tracebacks[host.name] = traceback.format_exc() + tb = traceback.format_exc() + r = Result(host, exception=e, result=tb, failed=True) + result[host.name] = r + self.logger.error("{}: {}".format(host, tb)) return result def _run_parallel(self, task, num_workers, dry_run, **kwargs): @@ -142,6 +171,9 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): """ num_workers = num_workers or self.config.num_workers + self.logger.info("Running task '{}' with num_workers: {}, dry_run: {}".format( + kwargs.get("name") or task.__name__, num_workers, dry_run)) + self.logger.debug(kwargs) if num_workers == 1: result = self._run_serial(task, dry_run, **kwargs) else: @@ -153,10 +185,11 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): def run_task(host, brigade, dry_run, task): + logger = logging.getLogger("brigade") try: - logger.debug("{}: running task {}".format(host.name, task)) + logger.info("{}: {}: running task".format(host.name, task.name)) r = task._start(host=host, brigade=brigade, dry_run=dry_run) return host.name, r, None, None except Exception as e: - logger.error("{}: {}".format(host, e)) + logger.error("{}: {}".format(host, traceback.format_exc())) return host.name, None, e, traceback.format_exc() From 5e59282e156b5b786fcf46a71c1892f4505f541e Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 27 Dec 2017 19:42:36 +0100 Subject: [PATCH 11/31] better error handling --- brigade/core/__init__.py | 38 +++++++------------ brigade/core/exceptions.py | 18 ++++----- brigade/core/inventory.py | 4 +- brigade/core/task.py | 27 +++++++------ tests/core/test_multithreading.py | 8 ++-- tests/plugins/tasks/commands/test_command.py | 8 ++-- .../tasks/commands/test_remote_command.py | 4 +- tests/plugins/tasks/data/test_load_json.py | 8 ++-- tests/plugins/tasks/data/test_load_yaml.py | 8 ++-- .../tasks/networking/test_napalm_cli.py | 4 +- .../tasks/networking/test_napalm_configure.py | 4 +- .../tasks/networking/test_napalm_get.py | 4 +- .../plugins/tasks/networking/test_tcp_ping.py | 8 ++-- .../plugins/tasks/text/test_template_file.py | 4 +- .../tasks/text/test_template_string.py | 4 +- 15 files changed, 73 insertions(+), 78 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 8583c77b..9b304723 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -5,7 +5,7 @@ from multiprocessing.dummy import Pool from brigade.core.configuration import Config -from brigade.core.task import AggregatedResult, Task +from brigade.core.task import AggregatedResult, Result, Task from brigade.plugins.tasks import connections @@ -119,36 +119,24 @@ def filter(self, **kwargs): return b def _run_serial(self, task, dry_run, **kwargs): - t = Task(task, **kwargs) - result = AggregatedResult() + result = AggregatedResult(kwargs.get("name") or task.__name__) for host in self.inventory.hosts.values(): - try: - self.logger.info("{}: {}: running task".format(host.name, t.name)) - r = t._start(host=host, brigade=self, dry_run=dry_run) - result[host.name] = r - except Exception as e: - tb = traceback.format_exc() - r = Result(host, exception=e, result=tb, failed=True) - result[host.name] = r - self.logger.error("{}: {}".format(host, tb)) + result[host.name] = run_task(host, self, dry_run, Task(task, **kwargs)) return result def _run_parallel(self, task, num_workers, dry_run, **kwargs): - result = AggregatedResult() + result = AggregatedResult(kwargs.get("name") or task.__name__) pool = Pool(processes=num_workers) - result_pool = [pool.apply_async(run_task, args=(h, self, dry_run, Task(task, **kwargs))) + result_pool = [pool.apply_async(run_task, + args=(h, self, dry_run, Task(task, **kwargs))) for h in self.inventory.hosts.values()] pool.close() pool.join() - for r in result_pool: - host, res, exc, traceback = r.get() - if exc: - result.failed_hosts[host] = exc - result.tracebacks[host] = traceback - else: - result[host] = res + for rp in result_pool: + r = rp.get() + result[r.host.name] = r return result def run(self, task, num_workers=None, dry_run=None, **kwargs): @@ -174,6 +162,7 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): self.logger.info("Running task '{}' with num_workers: {}, dry_run: {}".format( kwargs.get("name") or task.__name__, num_workers, dry_run)) self.logger.debug(kwargs) + if num_workers == 1: result = self._run_serial(task, dry_run, **kwargs) else: @@ -189,7 +178,8 @@ def run_task(host, brigade, dry_run, task): try: logger.info("{}: {}: running task".format(host.name, task.name)) r = task._start(host=host, brigade=brigade, dry_run=dry_run) - return host.name, r, None, None except Exception as e: - logger.error("{}: {}".format(host, traceback.format_exc())) - return host.name, None, e, traceback.format_exc() + tb = traceback.format_exc() + logger.error("{}: {}".format(host, tb)) + r = Result(host, exception=e, result=tb, failed=True) + return r diff --git a/brigade/core/exceptions.py b/brigade/core/exceptions.py index d16efacd..e19e2532 100644 --- a/brigade/core/exceptions.py +++ b/brigade/core/exceptions.py @@ -32,19 +32,19 @@ class BrigadeExecutionError(Exception): """ def __init__(self, result): self.result = result - self.failed_hosts = result.failed_hosts - self.tracebacks = result.tracebacks + + @property + def failed_hosts(self): + return {k: v for k, v in self.result.items() if v.failed} def __str__(self): text = "\n" for k, r in self.result.items(): text += "{}\n".format("#" * 40) - text += "# {} (succeeded) \n".format(k) - text += "{}\n".format("#" * 40) - text += "{}\n".format(r) - for k, r in self.tracebacks.items(): - text += "{}\n".format("#" * 40) - text += "# {} (failed) \n".format(k) + if r.failed: + text += "# {} (failed)\n".format(k) + else: + text += "# {} (succeeded)\n".format(k) text += "{}\n".format("#" * 40) - text += "{}\n".format(r) + text += "{}\n".format(r.result) return text diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index adb5186e..b30b85b6 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -214,8 +214,8 @@ def get_connection(self, connection): # we are already inside a thread # Task should establish a connection and populate self.connection[connection] r = self.brigade.filter(name=self.name).run(conn_task, num_workers=1) - if self.name in r.failed_hosts: - raise r.failed_hosts[self.name] + if r[self.name].exception: + raise r[self.name].exception return self.connections[connection] diff --git a/brigade/core/task.py b/brigade/core/task.py index 34006e85..d6018725 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -22,20 +22,21 @@ class Task(object): dry_run(``bool``): Populated right before calling the ``task`` """ - def __init__(self, task, **kwargs): + def __init__(self, task, name=None, **kwargs): + self.name = name or task.__name__ self.task = task self.params = kwargs def __repr__(self): - return self.task.__name__ + return self.name def _start(self, host, brigade, dry_run): self.host = host self.brigade = brigade self.dry_run = dry_run if dry_run is not None else brigade.dry_run - return self.task(self, **self.params) + return self.task(self, **self.params) or Result(host) - def run(self, task, **kwargs): + def run(self, task, dry_run=None, **kwargs): """ This is a utility method to call a task from within a task. For instance: @@ -51,8 +52,7 @@ def grouped_tasks(task): msg = ("You have to call this after setting host and brigade attributes. ", "You probably called this from outside a nested task") raise Exception(msg) - aggr = self.brigade.filter(name=self.host.name).run(task, num_workers=1, **kwargs) - return aggr[self.host.name] + return Task(task, **kwargs)._start(self.host, self.brigade, dry_run) class Result(object): @@ -72,11 +72,14 @@ class Result(object): host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result """ - def __init__(self, host, result=None, changed=False, diff="", **kwargs): + def __init__(self, host, result=None, changed=False, diff="", failed=False, exception=None, + **kwargs): self.result = result self.host = host self.changed = changed self.diff = diff + self.failed = failed + self.exception = exception for k, v in kwargs.items(): setattr(self, k, v) @@ -90,15 +93,17 @@ class AggregatedResult(dict): Attributes: failed_hosts (list): list of hosts that failed """ - def __init__(self, **kwargs): + def __init__(self, name, **kwargs): + self.name = name super().__init__(**kwargs) - self.failed_hosts = {} - self.tracebacks = {} + + def __repr__(self): + return '{}: {}'.format(self.__class__.__name__, self.name) @property def failed(self): """If ``True`` at least a host failed.""" - return bool(self.failed_hosts) + return any([h.failed for h in self.values()]) def raise_on_error(self): """ diff --git a/tests/core/test_multithreading.py b/tests/core/test_multithreading.py index a03e4452..5c334d34 100644 --- a/tests/core/test_multithreading.py +++ b/tests/core/test_multithreading.py @@ -51,28 +51,28 @@ def test_failing_task_simple_singlethread(self, brigade): brigade.run(failing_task_simple, num_workers=1) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, Exception), v + assert isinstance(v.exception, Exception), v def test_failing_task_simple_multithread(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(failing_task_simple, num_workers=NUM_WORKERS) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, Exception), v + assert isinstance(v.exception, Exception), v def test_failing_task_complex_singlethread(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(failing_task_complex, num_workers=1) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, CommandError), v + assert isinstance(v.exception, CommandError), v def test_failing_task_complex_multithread(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(failing_task_complex, num_workers=NUM_WORKERS) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, CommandError), v + assert isinstance(v.exception, CommandError), v def test_change_data_in_thread(self, brigade): brigade.run(change_data, num_workers=NUM_WORKERS) diff --git a/tests/plugins/tasks/commands/test_command.py b/tests/plugins/tasks/commands/test_command.py index 312d559e..7ed7e6cd 100644 --- a/tests/plugins/tasks/commands/test_command.py +++ b/tests/plugins/tasks/commands/test_command.py @@ -18,13 +18,13 @@ def test_command_error(self, brigade): brigade.run(commands.command, command="ech") assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, OSError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, OSError) def test_command_error_generic(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(commands.command, command="ls /asdadsd") assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, CommandError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, CommandError) diff --git a/tests/plugins/tasks/commands/test_remote_command.py b/tests/plugins/tasks/commands/test_remote_command.py index 63d3b85f..9486745f 100644 --- a/tests/plugins/tasks/commands/test_remote_command.py +++ b/tests/plugins/tasks/commands/test_remote_command.py @@ -18,5 +18,5 @@ def test_remote_command_error_generic(self, brigade): brigade.run(commands.remote_command, command="ls /asdadsd") assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, CommandError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, CommandError) diff --git a/tests/plugins/tasks/data/test_load_json.py b/tests/plugins/tasks/data/test_load_json.py index 03e767ec..e53d778b 100644 --- a/tests/plugins/tasks/data/test_load_json.py +++ b/tests/plugins/tasks/data/test_load_json.py @@ -29,8 +29,8 @@ def test_load_json_error_broken_file(self, brigade): brigade.run(data.load_json, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) def test_load_json_error_missing_file(self, brigade): test_file = '{}/missing.json'.format(data_dir) @@ -43,5 +43,5 @@ def test_load_json_error_missing_file(self, brigade): brigade.run(data.load_json, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, not_found) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, not_found) diff --git a/tests/plugins/tasks/data/test_load_yaml.py b/tests/plugins/tasks/data/test_load_yaml.py index 15fbde6b..4421cb96 100644 --- a/tests/plugins/tasks/data/test_load_yaml.py +++ b/tests/plugins/tasks/data/test_load_yaml.py @@ -33,8 +33,8 @@ def test_load_yaml_error_broken_file(self, brigade): brigade.run(data.load_yaml, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ScannerError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ScannerError) def test_load_yaml_error_missing_file(self, brigade): test_file = '{}/missing.yaml'.format(data_dir) @@ -48,5 +48,5 @@ def test_load_yaml_error_missing_file(self, brigade): brigade.run(data.load_yaml, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, not_found) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, not_found) diff --git a/tests/plugins/tasks/networking/test_napalm_cli.py b/tests/plugins/tasks/networking/test_napalm_cli.py index 4eec82b5..7bc712ff 100644 --- a/tests/plugins/tasks/networking/test_napalm_cli.py +++ b/tests/plugins/tasks/networking/test_napalm_cli.py @@ -34,6 +34,6 @@ def test_napalm_cli(self, brigade): # "show interfacesa"], # optional_args=opt) # assert len(e.value.failed_hosts) - # for exc in e.value.failed_hosts.values(): - # assert isinstance(exc, exceptions.CommandErrorException) + # for result in e.value.failed_hosts.values(): + # assert isinstance(result.exception, exceptions.CommandErrorException) # print(exc) diff --git a/tests/plugins/tasks/networking/test_napalm_configure.py b/tests/plugins/tasks/networking/test_napalm_configure.py index 7d9cf0c7..fcbe0e9d 100644 --- a/tests/plugins/tasks/networking/test_napalm_configure.py +++ b/tests/plugins/tasks/networking/test_napalm_configure.py @@ -55,5 +55,5 @@ def test_napalm_configure_change_error(self, brigade): with pytest.raises(BrigadeExecutionError) as e: d.run(networking.napalm_configure, configuration=configuration) assert len(e.value.failed_hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, exceptions.MergeConfigException) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, exceptions.MergeConfigException) diff --git a/tests/plugins/tasks/networking/test_napalm_get.py b/tests/plugins/tasks/networking/test_napalm_get.py index d3520bb1..459e3933 100644 --- a/tests/plugins/tasks/networking/test_napalm_get.py +++ b/tests/plugins/tasks/networking/test_napalm_get.py @@ -33,5 +33,5 @@ def test_napalm_getters_error(self, brigade): getters=["facts", "interfaces"]) assert len(e.value.failed_hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, KeyError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, KeyError) diff --git a/tests/plugins/tasks/networking/test_tcp_ping.py b/tests/plugins/tasks/networking/test_tcp_ping.py index 0699e775..eb42993a 100644 --- a/tests/plugins/tasks/networking/test_tcp_ping.py +++ b/tests/plugins/tasks/networking/test_tcp_ping.py @@ -37,16 +37,16 @@ def test_tcp_ping_invalid_port(self, brigade): brigade.run(networking.tcp_ping, ports='web') assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) def test_tcp_ping_invalid_ports(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(networking.tcp_ping, ports=[22, 'web', 443]) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) def test_tcp_ping_external_hosts(): diff --git a/tests/plugins/tasks/text/test_template_file.py b/tests/plugins/tasks/text/test_template_file.py index ba17d60e..b7aa0070 100644 --- a/tests/plugins/tasks/text/test_template_file.py +++ b/tests/plugins/tasks/text/test_template_file.py @@ -33,5 +33,5 @@ def test_template_file_error_broken_file(self, brigade): template='broken.j2', path=data_dir) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, TemplateSyntaxError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, TemplateSyntaxError) diff --git a/tests/plugins/tasks/text/test_template_string.py b/tests/plugins/tasks/text/test_template_string.py index 52aef9b9..6919517a 100644 --- a/tests/plugins/tasks/text/test_template_string.py +++ b/tests/plugins/tasks/text/test_template_string.py @@ -49,5 +49,5 @@ def test_template_string_error_broken_string(self, brigade): brigade.run(text.template_string, template=broken_j2) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, TemplateSyntaxError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, TemplateSyntaxError) From 52f63f9d1c75ed773993e1893b1af019004f7fdb Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 27 Dec 2017 19:52:20 +0100 Subject: [PATCH 12/31] document new Result attributes --- brigade/core/task.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brigade/core/task.py b/brigade/core/task.py index d6018725..553ff367 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -64,12 +64,16 @@ class Result(object): diff (obj): Diff between state of the system before/after running this task result (obj): Result of the task execution, see task's documentation for details host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result + failed (bool): Whether the execution failed or not + exception (Exception): uncaught exception thrown during the exection of the task (if any) Attributes: changed (bool): ``True`` if the task is changing the system diff (obj): Diff between state of the system before/after running this task result (obj): Result of the task execution, see task's documentation for details host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result + failed (bool): Whether the execution failed or not + exception (Exception): uncaught exception thrown during the exection of the task (if any) """ def __init__(self, host, result=None, changed=False, diff="", failed=False, exception=None, From 4972edd6aa8ced42c5f9bed881a21bc5b9d07367 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Fri, 29 Dec 2017 18:55:35 +0100 Subject: [PATCH 13/31] allow overriding raise_on_error per task For instance: result = d.run(networking.napalm_validate, raise_on_error=False, src=THIS_DIR + "/data/validate_error.yaml") --- brigade/core/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 9b304723..e1cd473c 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -139,7 +139,7 @@ def _run_parallel(self, task, num_workers, dry_run, **kwargs): result[r.host.name] = r return result - def run(self, task, num_workers=None, dry_run=None, **kwargs): + def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwargs): """ Run task over all the hosts in the inventory. @@ -148,6 +148,7 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): the inventory num_workers(``int``): Override for how many hosts to run in paralell for this task dry_run(``bool``): Whether if we are testing the changes or not + raise_on_error (``bool``): Override raise_on_error behavior **kwargs: additional argument to pass to ``task`` when calling it Raises: @@ -168,7 +169,9 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): else: result = self._run_parallel(task, num_workers, dry_run, **kwargs) - if self.config.raise_on_error: + raise_on_error = raise_on_error if raise_on_error is not None else \ + self.config.raise_on_error + if raise_on_error: result.raise_on_error() return result From cecc260724cd9cebcb3dc30cdc219388681b6b34 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Fri, 29 Dec 2017 19:13:37 +0100 Subject: [PATCH 14/31] added napalm_validate task --- brigade/plugins/tasks/networking/__init__.py | 2 + .../tasks/networking/napalm_validate.py | 22 +++++++++ .../tasks/networking/data/validate_error.yaml | 6 +++ .../tasks/networking/data/validate_ok.yaml | 6 +++ .../tasks/networking/test_napalm_validate.py | 49 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 brigade/plugins/tasks/networking/napalm_validate.py create mode 100644 tests/plugins/tasks/networking/data/validate_error.yaml create mode 100644 tests/plugins/tasks/networking/data/validate_ok.yaml create mode 100644 tests/plugins/tasks/networking/test_napalm_validate.py diff --git a/brigade/plugins/tasks/networking/__init__.py b/brigade/plugins/tasks/networking/__init__.py index 2b62194e..59e33ed9 100644 --- a/brigade/plugins/tasks/networking/__init__.py +++ b/brigade/plugins/tasks/networking/__init__.py @@ -1,6 +1,7 @@ from .napalm_cli import napalm_cli from .napalm_configure import napalm_configure from .napalm_get import napalm_get +from .napalm_validate import napalm_validate from .netmiko_send_command import netmiko_send_command from .tcp_ping import tcp_ping @@ -8,6 +9,7 @@ "napalm_cli", "napalm_configure", "napalm_get", + "napalm_validate", "netmiko_send_command", "tcp_ping", ) diff --git a/brigade/plugins/tasks/networking/napalm_validate.py b/brigade/plugins/tasks/networking/napalm_validate.py new file mode 100644 index 00000000..76e9ab9d --- /dev/null +++ b/brigade/plugins/tasks/networking/napalm_validate.py @@ -0,0 +1,22 @@ +from brigade.core.task import Result + + +def napalm_validate(task, src=None, validation_source=None): + """ + Gather information with napalm and validate it: + + http://napalm.readthedocs.io/en/develop/validate/index.html + + Arguments: + src: file to use as validation source + validation_source (list): instead of a file data needed to validate device's state + + Returns: + :obj:`brigade.core.task.Result`: + * result (``dict``): dictionary with the result of the validation + * failed (``bool``): Whether the device complies or not (note this will trigger a + :obj:`brigade.core.exceptions.BrigadeExecutionError` if raise_on_error is set to ``True``) + """ + device = task.host.get_connection("napalm") + r = device.compliance_report(validation_file=src, validation_source=validation_source) + return Result(host=task.host, result=r, failed=not r["complies"]) diff --git a/tests/plugins/tasks/networking/data/validate_error.yaml b/tests/plugins/tasks/networking/data/validate_error.yaml new file mode 100644 index 00000000..9ecfe461 --- /dev/null +++ b/tests/plugins/tasks/networking/data/validate_error.yaml @@ -0,0 +1,6 @@ +--- +- get_facts: + os_version: 4.15.5M-3054042.4155M +- get_interfaces: + Ethernet1: + description: "unset" diff --git a/tests/plugins/tasks/networking/data/validate_ok.yaml b/tests/plugins/tasks/networking/data/validate_ok.yaml new file mode 100644 index 00000000..68d68381 --- /dev/null +++ b/tests/plugins/tasks/networking/data/validate_ok.yaml @@ -0,0 +1,6 @@ +--- +- get_facts: + os_version: 4.15.5M-3054042.4155M +- get_interfaces: + Ethernet1: + description: "" diff --git a/tests/plugins/tasks/networking/test_napalm_validate.py b/tests/plugins/tasks/networking/test_napalm_validate.py new file mode 100644 index 00000000..ed899066 --- /dev/null +++ b/tests/plugins/tasks/networking/test_napalm_validate.py @@ -0,0 +1,49 @@ +import os + +from brigade.plugins.tasks import connections, networking + + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + + +class Test(object): + + def test_napalm_validate_src_ok(self, brigade): + opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} + print(opt["path"]) + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_validate, + src=THIS_DIR + "/data/validate_ok.yaml") + assert result + for h, r in result.items(): + assert not r.failed + + def test_napalm_validate_src_error(self, brigade): + opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} + print(opt["path"]) + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_validate, + src=THIS_DIR + "/data/validate_error.yaml") + assert result + for h, r in result.items(): + assert r.failed + assert not r.result["complies"] + + def test_napalm_validate_src_validate_source(self, brigade): + opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} + print(opt["path"]) + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + + validation_dict = [ + {"get_interfaces": {"Ethernet1": {"description": ""}}} + ] + + result = d.run(networking.napalm_validate, + validation_source=validation_dict) + + assert result + for h, r in result.items(): + assert not r.failed From 30e8570e77b3da4257126e2247ddd5ab2a587069 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Fri, 29 Dec 2017 19:14:35 +0100 Subject: [PATCH 15/31] render automatically template path --- brigade/plugins/tasks/text/template_file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brigade/plugins/tasks/text/template_file.py b/brigade/plugins/tasks/text/template_file.py index 66207780..9b8d8a78 100644 --- a/brigade/plugins/tasks/text/template_file.py +++ b/brigade/plugins/tasks/text/template_file.py @@ -17,6 +17,7 @@ def template_file(task, template, path, **kwargs): """ merged = merge_two_dicts(task.host, kwargs) path = format_string(path, task, **kwargs) + template = format_string(template, task, **kwargs) text = jinja_helper.render_from_file(template=template, path=path, host=task.host, **merged) return Result(host=task.host, result=text) From 0b265ce984474acbfe9691a91aefcd49ed511fc1 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Fri, 29 Dec 2017 19:19:30 +0100 Subject: [PATCH 16/31] Added helper functions to make output pretty --- brigade/plugins/functions/__init__.py | 6 +++ brigade/plugins/functions/text/__init__.py | 12 ++++++ brigade/plugins/tasks/text/__init__.py | 2 + brigade/plugins/tasks/text/print_result.py | 45 ++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 66 insertions(+) create mode 100644 brigade/plugins/functions/__init__.py create mode 100644 brigade/plugins/functions/text/__init__.py create mode 100644 brigade/plugins/tasks/text/print_result.py diff --git a/brigade/plugins/functions/__init__.py b/brigade/plugins/functions/__init__.py new file mode 100644 index 00000000..419dd70e --- /dev/null +++ b/brigade/plugins/functions/__init__.py @@ -0,0 +1,6 @@ +from . import text + + +__all__ = ( + "text", +) diff --git a/brigade/plugins/functions/text/__init__.py b/brigade/plugins/functions/text/__init__.py new file mode 100644 index 00000000..b7a96615 --- /dev/null +++ b/brigade/plugins/functions/text/__init__.py @@ -0,0 +1,12 @@ +from colorama import Fore, Style, init + + +init(autoreset=True) + + +def print_title(title): + """ + Helper function to print a title. + """ + msg = "**** {} ".format(title) + print("{}{}{}{}".format(Style.BRIGHT, Fore.GREEN, msg, "*" * (80 - len(msg)))) diff --git a/brigade/plugins/tasks/text/__init__.py b/brigade/plugins/tasks/text/__init__.py index dfc5efa3..3a41db10 100644 --- a/brigade/plugins/tasks/text/__init__.py +++ b/brigade/plugins/tasks/text/__init__.py @@ -1,7 +1,9 @@ +from .print_result import print_result from .template_file import template_file from .template_string import template_string __all__ = ( + "print_result", "template_file", "template_string", ) diff --git a/brigade/plugins/tasks/text/print_result.py b/brigade/plugins/tasks/text/print_result.py new file mode 100644 index 00000000..337b1018 --- /dev/null +++ b/brigade/plugins/tasks/text/print_result.py @@ -0,0 +1,45 @@ +import pprint + +from brigade.core.task import AggregatedResult, Result + +from colorama import Fore, Style, init + + +init(autoreset=True) + + +def print_result(task, data, vars=None): + """ + Prints on screen the :obj:`brigade.core.task.Result` from a previous task + + Arguments: + data (:obj:`brigade.core.task.Result`): from a previous task + vars (list of str): Which attributes you want to print + + Returns: + :obj:`brigade.core.task.Result`: + """ + vars = vars or ["diff", "result", "stdout"] + if isinstance(vars, str): + vars = [vars] + + if isinstance(data, AggregatedResult): + data = data[task.host.name] + + if data.failed: + color = Fore.RED + elif data.changed: + color = Fore.YELLOW + else: + color = Fore.BLUE + changed = "" if data.changed is None else " ** changed : {} ".format(data.changed) + msg = "* {}{}".format(task.host.name, changed) + print("{}{}{}{}".format(Style.BRIGHT, color, msg, "*" * (80 - len(msg)))) + for v in vars: + r = getattr(data, v, "") + if r and not isinstance(r, str): + pprint.pprint(r) + elif r: + print(r) + + return Result(task.host) diff --git a/requirements.txt b/requirements.txt index 235d09d0..2efafbfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +colorama pyyaml jinja2 napalm>=2.2.0 From 3e89f8c65560a0cacb26ba2943c0e49d9226407b Mon Sep 17 00:00:00 2001 From: David Barroso Date: Fri, 29 Dec 2017 19:24:16 +0100 Subject: [PATCH 17/31] fix napalm_validate tests --- tests/inventory_data/nsot/nsot.sqlite3 | Bin 228352 -> 245760 bytes .../tasks/networking/test_napalm_validate.py | 1 + 2 files changed, 1 insertion(+) diff --git a/tests/inventory_data/nsot/nsot.sqlite3 b/tests/inventory_data/nsot/nsot.sqlite3 index 5fd8479baee4161f562baafe6f0233a43985d474..a410603ce9a25489080862fa4ad1b2a65b5defc3 100644 GIT binary patch delta 6818 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 Sj$aX21h!4>`fondu=zir1EXL7 delta 733 zcmZuuOH5Ny5WO?=fI?ajMQj2|YviMbl$Q6>7F1N?0xCjcWMjH`{2^7UiJ;56&{Rbz zVlskmMB+vyn(BKI65JUV$O0uwNF}&(p&J(p#Taj)8xxb8n>#c2%$aj063L6K+hRo- znh;`j#Ca67tNKeFCc7;q)F^Xem90HcAJ4DYM6)>rhsyIkPPey4_0_0umtWO1t;)QA z2YPq!P$-xJyr+*eO+mUQF((OL^H6|F6P1iGOp6` z;Aq6!!a*GaJfFG_w@SJaIt9G&1)ZSsVk?F@-iDX>Vml7=$`dH%)(&jq4;>gQ%aJs8 z5f!rKqWA^Ha9l*83vR(PSb`)v@C5ea5bxJflSD4Q-Gv8up^BWm-ihOEcHs*i>BexH z@~osR;;D6PX6FVr@X!W^*pbA5(Su5brC2-=^2~Bh3}f7UgR=PREadUrb+nkvcd?wd z6!Y9pvVh;Xhl+0B1$6@Ipc@9^`Tu76!OKah@cbr@vQLUjPtr%ujNWY@7SYK+>#3h( z4b;c`0u(ZS<2$$ct%3?9VI|qKe45we@tiT-lxerr3Q;TCMRd#KTslW1WYCHb+uiDD zFAomTD-K Date: Sun, 31 Dec 2017 16:13:06 +0100 Subject: [PATCH 18/31] bugfix --- brigade/core/helpers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brigade/core/helpers/__init__.py b/brigade/core/helpers/__init__.py index 652792b4..67514857 100644 --- a/brigade/core/helpers/__init__.py +++ b/brigade/core/helpers/__init__.py @@ -2,7 +2,7 @@ def merge_two_dicts(x, y): try: z = x.copy() except AttributeError: - z = x.items() + z = dict(x) z.update(y) return z From ca49110704ac7c731e3a300dfd56ebeda12b39d0 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 31 Dec 2017 16:13:37 +0100 Subject: [PATCH 19/31] return properly dict_keys and dict_values --- brigade/core/inventory.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index b30b85b6..da7af8fe 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -73,19 +73,17 @@ def __init__(self, name, group=None, brigade=None, **kwargs): for k, v in kwargs.items(): self.data[k] = v + def _resolve_data(self): + d = self.group if self.group else {} + return helpers.merge_two_dicts(d, self.data) + def keys(self): """Returns the keys of the attribute ``data`` and of the parent(s) groups.""" - k = list(self.data.keys()) - if self.group: - k.extend(list(self.group.keys())) - return k + return self._resolve_data().keys() def values(self): """Returns the values of the attribute ``data`` and of the parent(s) groups.""" - v = list(self.data.values()) - if self.group: - v.extend(list(self.group.values())) - return v + return self._resolve_data().values() def __getitem__(self, item): try: @@ -128,11 +126,7 @@ def items(self): Returns all the data accessible from a device, including the one inherited from parent groups """ - if self.group: - d = self.group.items() - else: - d = {} - return helpers.merge_two_dicts(d, self.data) + return self._resolve_data().items() @property def brigade(self): From 1e06a18fd8e7bfe7c983824d60352a89c5a25e52 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sat, 6 Jan 2018 13:27:49 +0100 Subject: [PATCH 20/31] subtasks now return MultiResults which aggregate mutiple results for the same host --- brigade/core/__init__.py | 4 +- brigade/core/task.py | 55 +++++++++++++++++++--- brigade/plugins/tasks/text/print_result.py | 20 ++++---- tests/core/test_tasks.py | 8 ++-- 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index e1cd473c..1f24293b 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -185,4 +185,6 @@ def run_task(host, brigade, dry_run, task): tb = traceback.format_exc() logger.error("{}: {}".format(host, tb)) r = Result(host, exception=e, result=tb, failed=True) - return r + task.results.append(r) + + return task.results diff --git a/brigade/core/task.py b/brigade/core/task.py index 553ff367..cdff5290 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -26,15 +26,23 @@ def __init__(self, task, name=None, **kwargs): self.name = name or task.__name__ self.task = task self.params = kwargs + self.results = MultiResult(self.name) def __repr__(self): return self.name - def _start(self, host, brigade, dry_run): + def _start(self, host, brigade, dry_run, sub_task=False): self.host = host self.brigade = brigade self.dry_run = dry_run if dry_run is not None else brigade.dry_run - return self.task(self, **self.params) or Result(host) + r = self.task(self, **self.params) or Result(host) + r.name = self.name + + if sub_task: + return r + else: + self.results.insert(0, r) + return self.results def run(self, task, dry_run=None, **kwargs): """ @@ -52,12 +60,18 @@ def grouped_tasks(task): msg = ("You have to call this after setting host and brigade attributes. ", "You probably called this from outside a nested task") raise Exception(msg) - return Task(task, **kwargs)._start(self.host, self.brigade, dry_run) + r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True) + + if isinstance(r, MultiResult): + self.results.extend(r) + else: + self.results.append(r) + return r class Result(object): """ - Returned by tasks. + Result of running individual tasks. Arguments: changed (bool): ``True`` if the task is changing the system @@ -93,9 +107,6 @@ class AggregatedResult(dict): """ It basically is a dict-like object that aggregates the results for all devices. You can access each individual result by doing ``my_aggr_result["hostname_of_device"]``. - - Attributes: - failed_hosts (list): list of hosts that failed """ def __init__(self, name, **kwargs): self.name = name @@ -116,3 +127,33 @@ def raise_on_error(self): """ if self.failed: raise BrigadeExecutionError(self) + + +class MultiResult(list): + """ + It is basically is a list-like object that gives you access to the results of all subtasks for + a particular device/task. + """ + def __init__(self, name): + self.name = name + + def __getattr__(self, name): + return getattr(self[0], name) + + @property + def failed(self): + """If ``True`` at least a task failed.""" + return any([h.failed for h in self]) + + @property + def changed(self): + """If ``True`` at least a task changed the system.""" + return any([h.changed for h in self]) + + def raise_on_error(self): + """ + Raises: + :obj:`brigade.core.exceptions.BrigadeExecutionError`: When at least a task failed + """ + if self.failed: + raise BrigadeExecutionError(self) diff --git a/brigade/plugins/tasks/text/print_result.py b/brigade/plugins/tasks/text/print_result.py index 337b1018..32d19248 100644 --- a/brigade/plugins/tasks/text/print_result.py +++ b/brigade/plugins/tasks/text/print_result.py @@ -32,14 +32,18 @@ def print_result(task, data, vars=None): color = Fore.YELLOW else: color = Fore.BLUE - changed = "" if data.changed is None else " ** changed : {} ".format(data.changed) - msg = "* {}{}".format(task.host.name, changed) + title = "" if data.changed is None else " ** changed : {} ".format(data.changed) + msg = "* {}{}".format(task.host.name, title) print("{}{}{}{}".format(Style.BRIGHT, color, msg, "*" * (80 - len(msg)))) - for v in vars: - r = getattr(data, v, "") - if r and not isinstance(r, str): - pprint.pprint(r) - elif r: - print(r) + for r in data: + subtitle = "" if r.changed is None else " ** changed : {} ".format(r.changed) + msg = "---- {}{} ".format(r.name, subtitle) + print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg)))) + for v in vars: + x = getattr(r, v, "") + if r and not isinstance(x, str): + pprint.pprint(x) + elif r: + print(x) return Result(task.host) diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index 93a942ea..8487ca06 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -2,8 +2,8 @@ def sub_task(task): - return task.run(commands.command, - command="echo {}".format(task.host)) + task.run(commands.command, + command="echo {}".format(task.host)) class Test(object): @@ -19,4 +19,6 @@ def test_sub_task(self, brigade): result = brigade.run(sub_task) assert result for h, r in result.items(): - assert h == r.stdout.strip() + assert r[0].name == "sub_task" + assert r[1].name == "command" + assert h == r[1].stdout.strip() From f8302866ffc69b5952bdaf253ac6411e609a40d5 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sat, 6 Jan 2018 18:23:07 +0100 Subject: [PATCH 21/31] skip hosts if they fail and raise_on_error is False --- brigade/core/__init__.py | 20 ++++++++++- brigade/core/task.py | 31 ++++++++++++++--- .../tasks/networking/napalm_validate.py | 5 ++- tests/core/test_tasks.py | 34 +++++++++++++++++++ .../tasks/networking/test_napalm_validate.py | 5 +-- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 1f24293b..fcb67d95 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -34,6 +34,19 @@ def _unpickle_method(func_name, obj, cls): copy_reg.pickle(types.MethodType, _pickle_method, _unpickle_method) +class Data(object): + """ + This class is just a placeholder to share data amongsts different + versions of Brigade after running ``filter`` multiple times. + + Attributes: + failed_hosts (list): Hosts that have failed to run a task properly + """ + + def __init__(self): + self.failed_hosts = set() + + class Brigade(object): """ This is the main object to work with. It contains the inventory and it serves @@ -41,6 +54,7 @@ class Brigade(object): Arguments: inventory (:obj:`brigade.core.inventory.Inventory`): Inventory to work with + data(:obj:`brigade.core.Data`): shared data amongst different iterations of brigade dry_run(``bool``): Whether if we are testing the changes or not config (:obj:`brigade.core.configuration.Config`): Configuration object config_file (``str``): Path to Yaml configuration file @@ -49,15 +63,17 @@ class Brigade(object): Attributes: inventory (:obj:`brigade.core.inventory.Inventory`): Inventory to work with + data(:obj:`brigade.core.Data`): shared data amongst different iterations of brigade dry_run(``bool``): Whether if we are testing the changes or not config (:obj:`brigade.core.configuration.Config`): Configuration parameters available_connections (``dict``): dict of connection types are available """ def __init__(self, inventory, dry_run, config=None, config_file=None, - available_connections=None, logger=None): + available_connections=None, logger=None, data=None): self.logger = logger or logging.getLogger("brigade") + self.data = data or Data() self.inventory = inventory self.inventory.brigade = self @@ -173,6 +189,8 @@ def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwarg self.config.raise_on_error if raise_on_error: result.raise_on_error() + else: + self.data.failed_hosts.update(result.failed_hosts.keys()) return result diff --git a/brigade/core/task.py b/brigade/core/task.py index cdff5290..a65a0343 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -32,10 +32,13 @@ def __repr__(self): return self.name def _start(self, host, brigade, dry_run, sub_task=False): - self.host = host - self.brigade = brigade - self.dry_run = dry_run if dry_run is not None else brigade.dry_run - r = self.task(self, **self.params) or Result(host) + if host.name in brigade.data.failed_hosts: + r = Result(host, skipped=True) + else: + self.host = host + self.brigade = brigade + self.dry_run = dry_run if dry_run is not None else brigade.dry_run + r = self.task(self, **self.params) or Result(host) r.name = self.name if sub_task: @@ -80,6 +83,7 @@ class Result(object): host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result failed (bool): Whether the execution failed or not exception (Exception): uncaught exception thrown during the exection of the task (if any) + skipped (bool): ``True`` if the host was skipped Attributes: changed (bool): ``True`` if the task is changing the system @@ -88,16 +92,18 @@ class Result(object): host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result failed (bool): Whether the execution failed or not exception (Exception): uncaught exception thrown during the exection of the task (if any) + skipped (bool): ``True`` if the host was skipped """ def __init__(self, host, result=None, changed=False, diff="", failed=False, exception=None, - **kwargs): + skipped=False, **kwargs): self.result = result self.host = host self.changed = changed self.diff = diff self.failed = failed self.exception = exception + self.skipped = skipped for k, v in kwargs.items(): setattr(self, k, v) @@ -120,6 +126,16 @@ def failed(self): """If ``True`` at least a host failed.""" return any([h.failed for h in self.values()]) + @property + def failed_hosts(self): + """Hosts that failed during the execution of the task.""" + return {h: r for h, r in self.items() if r.failed} + + @property + def skipped(self): + """If ``True`` at least a host was skipped.""" + return any([h.skipped for h in self.values()]) + def raise_on_error(self): """ Raises: @@ -145,6 +161,11 @@ def failed(self): """If ``True`` at least a task failed.""" return any([h.failed for h in self]) + @property + def skipped(self): + """If ``True`` at least a host was skipped.""" + return any([h.skipped for h in self]) + @property def changed(self): """If ``True`` at least a task changed the system.""" diff --git a/brigade/plugins/tasks/networking/napalm_validate.py b/brigade/plugins/tasks/networking/napalm_validate.py index 76e9ab9d..e156b9e8 100644 --- a/brigade/plugins/tasks/networking/napalm_validate.py +++ b/brigade/plugins/tasks/networking/napalm_validate.py @@ -14,9 +14,8 @@ def napalm_validate(task, src=None, validation_source=None): Returns: :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the result of the validation - * failed (``bool``): Whether the device complies or not (note this will trigger a - :obj:`brigade.core.exceptions.BrigadeExecutionError` if raise_on_error is set to ``True``) + * complies (``bool``): Whether the device complies or not """ device = task.host.get_connection("napalm") r = device.compliance_report(validation_file=src, validation_source=validation_source) - return Result(host=task.host, result=r, failed=not r["complies"]) + return Result(host=task.host, result=r) diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index 8487ca06..a3ac2ede 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -1,6 +1,16 @@ from brigade.plugins.tasks import commands +def task_fails_for_some(task): + if task.host.name == "dev3.group_2": + # let's hardcode a failure + task.run(commands.command, + command="sasdasdasd") + else: + task.run(commands.command, + command="echo {}".format(task.host)) + + def sub_task(task): task.run(commands.command, command="echo {}".format(task.host)) @@ -22,3 +32,27 @@ def test_sub_task(self, brigade): assert r[0].name == "sub_task" assert r[1].name == "command" assert h == r[1].stdout.strip() + + def test_skip_failed_host(self, brigade): + result = brigade.run(task_fails_for_some, raise_on_error=False) + assert result.failed + assert not result.skipped + for h, r in result.items(): + if h == "dev3.group_2": + assert r.failed + else: + assert not r.failed + assert h == r[1].stdout.strip() + + result = brigade.run(task_fails_for_some) + assert not result.failed + assert result.skipped + for h, r in result.items(): + if h == "dev3.group_2": + assert r.skipped + else: + assert not r.skipped + assert h == r[1].stdout.strip() + + # let's reset it + brigade.data.failed_hosts = set() diff --git a/tests/plugins/tasks/networking/test_napalm_validate.py b/tests/plugins/tasks/networking/test_napalm_validate.py index f2c1c159..5a6c3b0a 100644 --- a/tests/plugins/tasks/networking/test_napalm_validate.py +++ b/tests/plugins/tasks/networking/test_napalm_validate.py @@ -24,12 +24,12 @@ def test_napalm_validate_src_error(self, brigade): print(opt["path"]) d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_validate, - raise_on_error=False, src=THIS_DIR + "/data/validate_error.yaml") assert result for h, r in result.items(): - assert r.failed + assert not r.failed assert not r.result["complies"] def test_napalm_validate_src_validate_source(self, brigade): @@ -48,3 +48,4 @@ def test_napalm_validate_src_validate_source(self, brigade): assert result for h, r in result.items(): assert not r.failed + assert r.result["complies"] From 8b8a4d681af5c5c7e37115a56291e63463496290 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sat, 6 Jan 2018 20:01:52 +0100 Subject: [PATCH 22/31] added easy_brigade --- brigade/easy.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 brigade/easy.py diff --git a/brigade/easy.py b/brigade/easy.py new file mode 100644 index 00000000..f05ba6aa --- /dev/null +++ b/brigade/easy.py @@ -0,0 +1,23 @@ +from brigade.core import Brigade +from brigade.core.configuration import Config +from brigade.plugins.inventory.simple import SimpleInventory + + +def easy_brigade(host_file="host.yaml", group_file="groups.yaml", dry_run=True, **kwargs): + """ + Helper function to create easily a :obj:`brigade.core.Brigade` object. + + Arguments: + host_file (str): path to the host file that will be passed to + :obj:`brigade.plugins.inventory.simple.SimpleInventory` + group_file (str): path to the group file that will be passed to + :obj:`brigade.plugins.inventory.simple.SimpleInventory` + dry_run (bool): whether if this is a dry run or not + **kwargs: Configuration parameters, see + :doc:`configuration parameters ` + """ + return Brigade( + inventory=SimpleInventory(host_file, group_file), + dry_run=dry_run, + config=Config(**kwargs), + ) From a7096c65f30409676df56ae4c5be3361fca75d30 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sat, 6 Jan 2018 20:02:50 +0100 Subject: [PATCH 23/31] added tutorials --- docs/howto/basic-napalm-getters.rst | 4 +- docs/ref/api/brigade.rst | 7 + docs/ref/api/easy.rst | 6 + docs/ref/api/index.rst | 3 +- docs/ref/api/task.rst | 7 + docs/ref/functions/index.rst | 8 + docs/ref/functions/text.rst | 6 + docs/ref/index.rst | 1 + docs/ref/inventory/index.rst | 2 + docs/tutorials/intro/brigade.rst | 74 ++++ docs/tutorials/intro/explore.rst | 2 - docs/tutorials/intro/index.rst | 9 +- docs/tutorials/intro/inventory.rst | 133 +++++- docs/tutorials/intro/run.rst | 2 - docs/tutorials/intro/running_tasks.rst | 89 ++++ .../intro/running_tasks_different_hosts.rst | 72 ++++ docs/tutorials/intro/running_tasks_errors.rst | 390 ++++++++++++++++++ .../intro/running_tasks_grouping.rst | 227 ++++++++++ examples/1_simple_runbooks/get_facts.py | 26 +- examples/{ => inventory}/Vagrantfile | 0 examples/{ => inventory}/groups.yaml | 0 examples/{ => inventory}/hosts.yaml | 0 .../{ => inventory}/network_diagram.graffle | Bin examples/{ => inventory}/network_diagram.png | Bin 24 files changed, 1045 insertions(+), 23 deletions(-) create mode 100644 docs/ref/api/easy.rst create mode 100644 docs/ref/functions/index.rst create mode 100644 docs/ref/functions/text.rst create mode 100644 docs/tutorials/intro/brigade.rst delete mode 100644 docs/tutorials/intro/explore.rst delete mode 100644 docs/tutorials/intro/run.rst create mode 100644 docs/tutorials/intro/running_tasks.rst create mode 100644 docs/tutorials/intro/running_tasks_different_hosts.rst create mode 100644 docs/tutorials/intro/running_tasks_errors.rst create mode 100644 docs/tutorials/intro/running_tasks_grouping.rst rename examples/{ => inventory}/Vagrantfile (100%) rename examples/{ => inventory}/groups.yaml (100%) rename examples/{ => inventory}/hosts.yaml (100%) rename examples/{ => inventory}/network_diagram.graffle (100%) rename examples/{ => inventory}/network_diagram.png (100%) diff --git a/docs/howto/basic-napalm-getters.rst b/docs/howto/basic-napalm-getters.rst index d72e0c6f..dbd65e6e 100644 --- a/docs/howto/basic-napalm-getters.rst +++ b/docs/howto/basic-napalm-getters.rst @@ -8,13 +8,13 @@ Let's start by seeing how to work with the inventory. Let's assume the following * Hosts file: -.. literalinclude:: ../../examples/hosts.yaml +.. literalinclude:: ../../examples/inventory/hosts.yaml :name: hosts.yaml :language: yaml * Groups file: -.. literalinclude:: ../../examples/groups.yaml +.. literalinclude:: ../../examples/inventory/groups.yaml :name: groups.yaml :language: yaml diff --git a/docs/ref/api/brigade.rst b/docs/ref/api/brigade.rst index 94fdc833..0ea4f024 100644 --- a/docs/ref/api/brigade.rst +++ b/docs/ref/api/brigade.rst @@ -1,3 +1,10 @@ +Data +#### + +.. autoclass:: brigade.core.Data + :members: + :undoc-members: + Brigade ####### diff --git a/docs/ref/api/easy.rst b/docs/ref/api/easy.rst new file mode 100644 index 00000000..02de78a5 --- /dev/null +++ b/docs/ref/api/easy.rst @@ -0,0 +1,6 @@ +Easy +==== + +.. automodule:: brigade.easy + :members: + :undoc-members: diff --git a/docs/ref/api/index.rst b/docs/ref/api/index.rst index a53ebe21..7ca6e828 100644 --- a/docs/ref/api/index.rst +++ b/docs/ref/api/index.rst @@ -8,5 +8,6 @@ Brigade API Reference brigade configuration inventory + easy task - exceptions \ No newline at end of file + exceptions diff --git a/docs/ref/api/task.rst b/docs/ref/api/task.rst index 2a86f698..8ccf2b8e 100644 --- a/docs/ref/api/task.rst +++ b/docs/ref/api/task.rst @@ -18,3 +18,10 @@ AggregatedResult .. autoclass:: brigade.core.task.AggregatedResult :members: :undoc-members: + +MultiResult +################ + +.. autoclass:: brigade.core.task.MultiResult + :members: + :undoc-members: diff --git a/docs/ref/functions/index.rst b/docs/ref/functions/index.rst new file mode 100644 index 00000000..cf781f1d --- /dev/null +++ b/docs/ref/functions/index.rst @@ -0,0 +1,8 @@ +Functions +========= + +.. toctree:: + :maxdepth: 2 + :glob: + + * diff --git a/docs/ref/functions/text.rst b/docs/ref/functions/text.rst new file mode 100644 index 00000000..6f62d224 --- /dev/null +++ b/docs/ref/functions/text.rst @@ -0,0 +1,6 @@ +Text +==== + +.. automodule:: brigade.plugins.functions.text + :members: + :undoc-members: diff --git a/docs/ref/index.rst b/docs/ref/index.rst index 7b3d72bb..9e863098 100644 --- a/docs/ref/index.rst +++ b/docs/ref/index.rst @@ -24,4 +24,5 @@ Reference Guides :caption: Plugins tasks/index + functions/index inventory/index diff --git a/docs/ref/inventory/index.rst b/docs/ref/inventory/index.rst index 5af16f3f..cf659aea 100644 --- a/docs/ref/inventory/index.rst +++ b/docs/ref/inventory/index.rst @@ -1,3 +1,5 @@ +.. _ref-inventory: + Inventory ========= diff --git a/docs/tutorials/intro/brigade.rst b/docs/tutorials/intro/brigade.rst new file mode 100644 index 00000000..ae5f7c10 --- /dev/null +++ b/docs/tutorials/intro/brigade.rst @@ -0,0 +1,74 @@ +Brigade +======= + +Now that we know how the inventory works let's create a brigade object we can start working with. There are two ways we can use: + +1. Using the :obj:`brigade.core.Brigade` directly, which is quite simple and the most flexible and versatile option. +2. Using :obj:`brigade.easy.easy_brigade`, which is simpler and good enough for most cases. + +Using the "raw" API +------------------- + +If you want to use the "raw" API you need two things: + +1. A configuration object. +2. An inventory object. + +Once you have them, you can create the brigade object yourself. For example:: + + >>> from brigade.core import Brigade + >>> from brigade.core.configuration import Config + >>> from brigade.plugins.inventory.simple import SimpleInventory + >>> + >>> brigade = Brigade( + ... inventory=SimpleInventory("hosts.yaml", "groups.yaml"), + ... dry_run=False, + ... config=Config(raise_on_error=False), + ... ) + >>> + +Using ``easy_brigade`` +---------------------- + +With :obj:`brigade.easy.easy_brigade` you only need to do:: + + >>> from brigade.easy import easy_brigade + >>> brigade = easy_brigade( + ... host_file="hosts.yaml", group_file="groups.yaml", + ... dry_run=True, + ... raise_on_error=False, + ... ) + >>> + +As you can see is not that different from above but you save a few imports. + +Brigade's Inventory +------------------- + +Brigade's object will always have a reference to the inventory you can inspect and work with if you have the need. For instance:: + + >>> brigade.inventory + + >>> brigade.inventory.hosts + {'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> brigade.inventory.groups + {'all': Group: all, 'bma': Group: bma, 'cmh': Group: cmh} + +As you will see further on in the tutorial you will rarely need to work with the inventory yourself as brigade will take care of it for you automatically but it's always good to know you have it there if you need to. + +Filtering the hosts +___________________ + +As we could see in the :doc:`Inventory ` section we could filter hosts based on data and attributes. The brigade object can leverage on that feature to "replicate" itself with subsets of devices allowing you to group your devices and perform actions on them as you see fit:: + + >>> switches = brigade.filter(type="network_device") + >>> switches.inventory.hosts + {'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> switches_in_bma = switches.filter(site="bma") + >>> switches_in_bma.inventory.hosts + {'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> hosts = brigade.filter(type="host") + >>> hosts.inventory.hosts + {'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma} + +All of the "replicas" of brigade will contain the same data and configuration, only the hosts will differ. diff --git a/docs/tutorials/intro/explore.rst b/docs/tutorials/intro/explore.rst deleted file mode 100644 index 08bda4be..00000000 --- a/docs/tutorials/intro/explore.rst +++ /dev/null @@ -1,2 +0,0 @@ -Exploring the inventory in Brigade -================================== diff --git a/docs/tutorials/intro/index.rst b/docs/tutorials/intro/index.rst index 68737c6c..850a458e 100644 --- a/docs/tutorials/intro/index.rst +++ b/docs/tutorials/intro/index.rst @@ -13,6 +13,9 @@ We're glad you made it here! This is a great place to learn the basics of Brigad Brigade at a glance 100% Python Installation guide - Creating an inventory - Exploring the inventory - Running tasks \ No newline at end of file + inventory + brigade + running_tasks + running_tasks_different_hosts + running_tasks_grouping + running_tasks_errors diff --git a/docs/tutorials/intro/inventory.rst b/docs/tutorials/intro/inventory.rst index 9a019474..5cd1b481 100644 --- a/docs/tutorials/intro/inventory.rst +++ b/docs/tutorials/intro/inventory.rst @@ -1,2 +1,131 @@ -Creating an inventory for Brigade -================================= +The Inventory +============= + +The inventory is arguably the most important piece of Brigade. The inventory organizes hosts and makes sure tasks have the correct data for each host. + +You can create the inventory in different ways, depending on your data source. To see the available plugins you can use go to the :ref:`ref-inventory` reference guide. + +.. note:: For this and the subsequent sections of this tutorial we are going to use the :obj:`SimpleInventory ` with the data located in ``/examples/inventory/``. We will also use the ``Vagrantfile`` located there so you should be able to reproduce everything. You can head to `Hosts/Groups contents`_ to see the contents of the file just for reference. + +First, let's create the inventory:: + + >>> from brigade.plugins.inventory.simple import SimpleInventory + >>> inventory = SimpleInventory(host_file="hosts.yaml", group_file="groups.yaml") + +Now let's inspect the hosts and groups we have:: + + >>> inventory.hosts + {'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> inventory.groups + {'all': Group: all, 'bma': Group: bma, 'cmh': Group: cmh} + +As you probably noticed both ``hosts`` and ``groups`` are dictionaries so you can iterate over them if you want to. + +Data +---- + +Let's start by grabbing a host: + + >>> h = inventory.hosts['host1.cmh'] + >>> print(h) + host1.cmh + +Now, let's check some attributes:: + + >>> h["site"] + 'cmh' + >>> h.data["role"] + 'host' + >>> h["domain"] + 'acme.com' + >>> h.data["domain"] + Traceback (most recent call last): + File "", line 1, in + KeyError: 'domain' + >>> h.group["domain"] + 'acme.com' + +What does this mean? You can access host data in two ways: + +1. As if the host was a dictionary, i.e., ``h["domain"]`` in which case the inventory will resolve the groups and use data inherited from them (in our example ``domain`` is coming from the parent group). +2. Via the ``data`` attribute in which case there is no group resolution going on so ``h["domain"]`` fails is that piece of data is not directly assigned to the host. + +Most of the time you will care about the first option but if you ever need to get data only from the host you can do it without a hassle. + +Finally, the host behaves like a python dictionary so you can iterate over the data as such:: + + >>> h.keys() + dict_keys(['name', 'group', 'asn', 'vlans', 'site', 'role', 'brigade_nos', 'type']) + >>> h.values() + dict_values(['host1.cmh', 'cmh', 65000, {100: 'frontend', 200: 'backend'}, 'cmh', 'host', 'linux', 'host']) + >>> h.items() + dict_items([('name', 'host1.cmh'), ('group', 'cmh'), ('asn', 65000), ('vlans', {100: 'frontend', 200: 'backend'}), ('site', 'cmh'), ('role', 'host'), ('brigade_nos', 'linux'), ('type', 'host')]) + >>> for k, v in h.items(): + ... print(k, v) + ... + name host1.cmh + group cmh + asn 65000 + vlans {100: 'frontend', 200: 'backend'} + site cmh + role host + brigade_nos linux + type host + >>> + +.. note:: You can head to :obj:`brigade.core.inventory.Host` and :obj:`brigade.core.inventory.Group` for details on all the available attributes and functions for each ``host`` and ``group``. + +Filtering the inventory +----------------------- + +You won't always want to operate over all hosts, sometimes you will want to operate over some of them based on some attributes. In order to do so the inventory can help you filtering based on it's attributes. For instance:: + + >>> inventory.hosts.keys() + dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh', 'host1.bma', 'host2.bma', 'spine00.bma', 'spine01.bma', 'leaf00.bma', 'leaf01.bma']) + >>> inventory.filter(site="bma").hosts.keys() + dict_keys(['host1.bma', 'host2.bma', 'spine00.bma', 'spine01.bma', 'leaf00.bma', 'leaf01.bma']) + >>> inventory.filter(site="bma", role="spine").hosts.keys() + dict_keys(['spine00.bma', 'spine01.bma']) + >>> inventory.filter(site="bma").filter(role="spine").hosts.keys() + dict_keys(['spine00.bma', 'spine01.bma']) + +Note in the last line that the filter is cumulative so you can do things like this: + + >>> cmh = inventory.filter(site="cmh") + >>> cmh.hosts.keys() + dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh']) + >>> cmh_eos = cmh.filter(brigade_nos="eos") + >>> cmh_eos.hosts.keys() + dict_keys(['spine00.cmh', 'leaf00.cmh']) + >>> cmh_eos.filter(role="spine").hosts.keys() + dict_keys(['spine00.cmh']) + +This should give you enough room to build groups in any way you want. + +Advanced filtering +__________________ + +You can also do more complex filtering by using functions or lambdas:: + + >>> def has_long_name(host): + ... return len(host.name) == 11 + ... + >>> inventory.filter(filter_func=has_long_name).hosts.keys() + dict_keys(['spine00.cmh', 'spine01.cmh', 'spine00.bma', 'spine01.bma']) + >>> inventory.filter(filter_func=lambda h: len(h.name) == 9).hosts.keys() + dict_keys(['host1.cmh', 'host2.cmh', 'host1.bma', 'host2.bma']) + +Not the most useful example but it should be enough to illustrate how it works. + + +Hosts/Groups contents +--------------------- + + +* ``hosts.yaml`` + +.. literalinclude:: ../../../examples/inventory/hosts.yaml + +* ``groups.yaml`` + +.. literalinclude:: ../../../examples/inventory/groups.yaml diff --git a/docs/tutorials/intro/run.rst b/docs/tutorials/intro/run.rst deleted file mode 100644 index a09cc28b..00000000 --- a/docs/tutorials/intro/run.rst +++ /dev/null @@ -1,2 +0,0 @@ -Running tasks with Brigade -========================== diff --git a/docs/tutorials/intro/running_tasks.rst b/docs/tutorials/intro/running_tasks.rst new file mode 100644 index 00000000..63edcbf9 --- /dev/null +++ b/docs/tutorials/intro/running_tasks.rst @@ -0,0 +1,89 @@ +Running tasks +============= + +Once you have your brigade objects you can start running :doc:`tasks `. The first thing you have to do is import the task you want to use:: + + >>> from brigade.plugins.tasks.commands import command + +Now you should be able to run that task for all devices:: + + >>> result = brigade.run(command, + ... command="echo hi! I am {host} and I am a {host.nos} device") + +.. note:: Note you can format strings using host data. + +This should give us a :obj:`brigade.core.task.AggregatedResult` object, which is a dictionary-like object where the key is the name of ``Host`` and the value a :obj:`brigade.core.task.Result`. + +Now, we can iterate over the object:: + + >>> for host, res in result.items(): + ... print(host + ": " + res.stdout) + ... + host1.cmh: hi! I am host1.cmh and I am a linux device + host2.cmh: hi! I am host2.cmh and I am a linux device + spine00.cmh: hi! I am spine00.cmh and I am a eos device + spine01.cmh: hi! I am spine01.cmh and I am a junos device + leaf00.cmh: hi! I am leaf00.cmh and I am a eos device + leaf01.cmh: hi! I am leaf01.cmh and I am a junos device + host1.bma: hi! I am host1.bma and I am a linux device + host2.bma: hi! I am host2.bma and I am a linux device + spine00.bma: hi! I am spine00.bma and I am a eos device + spine01.bma: hi! I am spine01.bma and I am a junos device + leaf00.bma: hi! I am leaf00.bma and I am a eos device + leaf01.bma: hi! I am leaf01.bma and I am a junos device + +Or we can use a task that knows how to operate on the :obj:`brigade.core.task.AggregatedResult` object like the task :obj:`brigade.plugins.tasks.text.print_result`:: + + >>> b.run(print_result, + ... num_workers=1, + ... data=result, + ... vars=["stdout"]) + * host1.cmh ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + hi! I am host1.cmh and I am a linux device + + * host2.cmh ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + hi! I am host2.cmh and I am a linux device + + * spine00.cmh ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + hi! I am spine00.cmh and I am a eos device + + * spine01.cmh ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + hi! I am spine01.cmh and I am a junos device + + * leaf00.cmh ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf00.cmh and I am a eos device + + * leaf01.cmh ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf01.cmh and I am a junos device + + * host1.bma ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + hi! I am host1.bma and I am a linux device + + * host2.bma ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + hi! I am host2.bma and I am a linux device + + * spine00.bma ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + hi! I am spine00.bma and I am a eos device + + * spine01.bma ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + hi! I am spine01.bma and I am a junos device + + * leaf00.bma ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf00.bma and I am a eos device + + * leaf01.bma ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf01.bma and I am a junos device + +.. note:: We need to pass ``num_workers=1`` to the ``print_result`` task because otherwise brigade will run each host at the same time using multithreading mangling the output. diff --git a/docs/tutorials/intro/running_tasks_different_hosts.rst b/docs/tutorials/intro/running_tasks_different_hosts.rst new file mode 100644 index 00000000..864d5b00 --- /dev/null +++ b/docs/tutorials/intro/running_tasks_different_hosts.rst @@ -0,0 +1,72 @@ +Running tasks on different groups of hosts +========================================== + +Below you can see an example where we use the ``filtering`` capabilities of ``brigade`` to run different tasks on different devices:: + + >>> switches = brigade.filter(type="network_device") + >>> hosts = brigade.filter(type="host") + >>> + >>> rs = switches.run(command, + ... command="echo I am a switch") + >>> + >>> rh = hosts.run(command, + ... command="echo I am a host") + +Because :obj:`brigade.core.task.AggregatedResult` objects behave like dictionaries you can add the results of the second task to the result of the first one:: + + >>> rs.update(rh) + +And then just print the result for all the devices:: + + >>> brigade.run(print_result, + ... num_workers=1, + ... data=rs, + ... vars=["stdout"]) + * host1.cmh ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + I am a host + + * host2.cmh ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + I am a host + + * spine00.cmh ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * spine01.cmh ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * leaf00.cmh ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * leaf01.cmh ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * host1.bma ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + I am a host + + * host2.bma ** changed : False ************************************************* + ---- command ** changed : False ----------------------------------------------- + I am a host + + * spine00.bma ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * spine01.bma ** changed : False *********************************************** + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * leaf00.bma ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + I am a switch + + * leaf01.bma ** changed : False ************************************************ + ---- command ** changed : False ----------------------------------------------- + I am a switch + diff --git a/docs/tutorials/intro/running_tasks_errors.rst b/docs/tutorials/intro/running_tasks_errors.rst new file mode 100644 index 00000000..1ef3d1ac --- /dev/null +++ b/docs/tutorials/intro/running_tasks_errors.rst @@ -0,0 +1,390 @@ +Dealing with task errors +======================== + +Tasks can fail due to many reasons. A continuation we will see how to deal with errors effectively with brigade. + +Failing by default +------------------ + +Brigade can raise a :obj:`brigade.core.exceptions.BrigadeExecutionError` exception automatically as soon as an error occurs. For instance:: + + >>> brigade = easy_brigade( + ... host_file="hosts.yaml", group_file="groups.yaml", + ... dry_run=True, + ... raise_on_error=True, + ... ) + >>> + >>> + >>> def task_that_sometimes_fails(task): + ... if task.host.name == "leaf00.cmh": + ... raise Exception("an uncontrolled exception happened") + ... elif task.host.name == "leaf01.cmh": + ... return Result(host=task.host, result="yikes", failed=True) + ... else: + ... return Result(host=task.host, result="swoosh") + ... + >>> + >>> b = brigade.filter(site="cmh") + >>> r = b.run(task_that_sometimes_fails) + Traceback (most recent call last): + File "", line 1, in + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run + result.raise_on_error() + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error + raise BrigadeExecutionError(self) + brigade.core.exceptions.BrigadeExecutionError: + ######################################## + # host1.cmh (succeeded) + ######################################## + swoosh + ######################################## + # host2.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine00.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine01.cmh (succeeded) + ######################################## + swoosh + ######################################## + # leaf00.cmh (failed) + ######################################## + Traceback (most recent call last): + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task + r = task._start(host=host, brigade=brigade, dry_run=dry_run) + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start + r = self.task(self, **self.params) or Result(host) + File "", line 3, in task_that_sometimes_fails + Exception: an uncontrolled exception happened + + ######################################## + # leaf01.cmh (failed) + ######################################## + yikes + +Ok, let's see what happened there. First, we configured the default behavior to raise an Exception as soon as an error occurs:: + + >>> brigade = easy_brigade( + ... host_file="hosts.yaml", group_file="groups.yaml", + ... dry_run=True, + ... raise_on_error=True, + ... ) + >>> + +Then, the following task fails with an exception for ``leaf00.cmh`` and with a controlled error on ``leaf01.cmh``. It doesn't matter if the error is controlled or not, both cases will trigger brigade to raise an Exception. + + >>> def task_that_sometimes_fails(task): + ... if task.host.name == "leaf00.cmh": + ... raise Exception("an uncontrolled exception happened") + ... elif task.host.name == "leaf01.cmh": + ... return Result(host=task.host, result="yikes", failed=True) + ... else: + ... return Result(host=task.host, result="swoosh") + ... + +Finally, when we run the task brigade fails immediately and the traceback is shown on the screen:: + + >>> b = brigade.filter(site="cmh") + >>> r = b.run(task_that_sometimes_fails) + Traceback (most recent call last): + File "", line 1, in + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run + result.raise_on_error() + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error + raise BrigadeExecutionError(self) + brigade.core.exceptions.BrigadeExecutionError: + ######################################## + # host1.cmh (succeeded) + ######################################## + swoosh + ######################################## + # host2.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine00.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine01.cmh (succeeded) + ######################################## + swoosh + ######################################## + # leaf00.cmh (failed) + ######################################## + Traceback (most recent call last): + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task + r = task._start(host=host, brigade=brigade, dry_run=dry_run) + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start + r = self.task(self, **self.params) or Result(host) + File "", line 3, in task_that_sometimes_fails + Exception: an uncontrolled exception happened + + ######################################## + # leaf01.cmh (failed) + ######################################## + yikes + +As with any other exception you can capture it:: + + >>> try: + ... r = b.run(task_that_sometimes_fails) + ... except BrigadeExecutionError as e: + ... error = e + ... + >>> + +Let's inspect the object. You can easily identify the tasks that failed:: + + >>> error.failed_hosts + {'leaf00.cmh': [], 'leaf01.cmh': []} + >>> error.failed_hosts['leaf00.cmh'][0].failed + True + >>> error.failed_hosts['leaf00.cmh'][0].result + 'Traceback (most recent call last):\n File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task\n r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start\n r = self.task(self, **self.params) or Result(host)\n File "", line 3, in task_that_sometimes_fails\nException: an uncontrolled exception happened\n' + >>> error.failed_hosts['leaf00.cmh'][0].exception + Exception('an uncontrolled exception happened',) + >>> error.failed_hosts['leaf01.cmh'][0].failed + True + >>> error.failed_hosts['leaf01.cmh'][0].result + 'yikes' + >>> error.failed_hosts['leaf01.cmh'][0].exception + >>> + +Or you can just grab the :obj:`brigade.core.task.AggregatedResult` inside the exception and do something useful with it:: + + >>> error.result.items() + dict_items([('host1.cmh', []), ('host2.cmh', []), ('spine00.cmh', []), ('spine01.cmh', []), ('leaf00.cmh', []), ('leaf01.cmh', [])]) + +Not failing by default +---------------------- + +Now, let's repeat the previous example but setting ``raise_on_error=False``:: + + >>> from brigade.core.task import Result + >>> from brigade.easy import easy_brigade + >>> from brigade.plugins.tasks.text import print_result + >>> + >>> brigade = easy_brigade( + ... host_file="hosts.yaml", group_file="groups.yaml", + ... dry_run=True, + ... raise_on_error=False, + ... ) + >>> + >>> + >>> def task_that_sometimes_fails(task): + ... if task.host.name == "leaf00.cmh": + ... raise Exception("an uncontrolled exception happened") + ... elif task.host.name == "leaf01.cmh": + ... return Result(host=task.host, result="yikes", failed=True) + ... else: + ... return Result(host=task.host, result="swoosh") + ... + >>> + >>> b = brigade.filter(site="cmh") + >>> + >>> r = b.run(task_that_sometimes_fails) + >>> + +If ``raise_on_error=False`` the result of the task will contain a :obj:`brigade.core.task.AggregatedResult` object describing what happened:: + + >>> r["leaf00.cmh"].failed + True + >>> r["leaf00.cmh"].result + 'Traceback (most recent call last):\n File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task\n r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start\n r = self.task(self, **self.params) or Result(host)\n File "", line 3, in task_that_sometimes_fails\nException: an uncontrolled exception happened\n' + >>> r["leaf00.cmh"].exception + Exception('an uncontrolled exception happened',) + >>> r["leaf01.cmh"].failed + True + >>> r["leaf01.cmh"].result + 'yikes' + >>> r["leaf01.cmh"].exception + >>> r["host1.cmh"].failed + False + >>> r["host1.cmh"].result + 'swoosh' + +Skipping Hosts +-------------- + +If you set ``raise_on_error=False`` and a task fails ``brigade`` will keep track of the failing hosts and will skip the host in following tasks:: + + >>> r = b.run(task_that_sometimes_fails) + >>> r.failed + True + >>> r.failed + False + +What did just happen? Let's inspect the result:: + + >>> r.skipped + True + >>> r['leaf00.cmh'].failed + False + >>> r['leaf00.cmh'].skipped + True + >>> r['leaf00.cmh'].result + >>> r['leaf01.cmh'].failed + False + >>> r['leaf01.cmh'].skipped + True + >>> r['leaf01.cmh'].result + >>> + +As you can see the second time we ran the same tasks didn't trigger any error because the hosts that failed the first time were skipped. You can inspect which devices are on the "blacklist":: + + >>> b.data.failed_hosts + {'leaf00.cmh', 'leaf01.cmh'} + +And even whitelist them: + + >>> r = b.run(task_that_sometimes_fails) + >>> r['leaf00.cmh'].skipped + True + >>> r['leaf01.cmh'].skipped + False + >>> r['leaf01.cmh'].failed + True + +You can also reset the list of blacklisted hosts:: + + >>> b.data.failed_hosts = set() + >>> r = b.run(task_that_sometimes_fails) + >>> r['leaf00.cmh'].skipped + False + >>> r['leaf00.cmh'].failed + True + >>> r['leaf01.cmh'].skipped + False + >>> r['leaf01.cmh'].failed + True + +``AggreggatedResult`` +--------------------- + +Regardless of if you had ``raise_on_error`` set to ``True`` or ``False`` you will have access to the very same :obj:`brigade.core.task.AggregatedResult` object. The only difference is that in the former case you will have the object in the ``result`` attribute of a :obj:`brigade.core.exceptions.BrigadeExecutionError` object and on the latter you will get it in the assigned variable. + +Let's see a few things you can do with an :obj:`brigade.core.task.AggregatedResult` object:: + + >>> r + AggregatedResult: task_that_sometimes_fails + >>> r.failed + True + >>> r.failed_hosts + {'leaf00.cmh': [], 'leaf01.cmh': []} + >>> r.raise_on_error() + Traceback (most recent call last): + File "", line 1, in + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error + raise BrigadeExecutionError(self) + brigade.core.exceptions.BrigadeExecutionError: + ######################################## + # host1.cmh (succeeded) + ######################################## + swoosh + ######################################## + # host2.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine00.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine01.cmh (succeeded) + ######################################## + swoosh + ######################################## + # leaf00.cmh (failed) + ######################################## + Traceback (most recent call last): + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task + r = task._start(host=host, brigade=brigade, dry_run=dry_run) + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start + r = self.task(self, **self.params) or Result(host) + File "", line 3, in task_that_sometimes_fails + Exception: an uncontrolled exception happened + + ######################################## + # leaf01.cmh (failed) + ######################################## + yikes + +As you can see you can quickly discern if the execution failed and you can even trigger the exception automatically if needed (if no host failed ``r.raise_on_error`` will just return ``None``) + +Overriding default behavior +--------------------------- + +Regardless of the default behavior you can force ``raise_on_error`` on a per task basis:: + + >>> r = b.run(task_that_sometimes_fails, + ... raise_on_error=True) + Traceback (most recent call last): + File "", line 2, in + r = b.run(task_that_sometimes_fails, + raise_on_error=False) + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run + result.raise_on_error() + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error + raise BrigadeExecutionError(self) + brigade.core.exceptions.BrigadeExecutionError: + ######################################## + # host1.cmh (succeeded) + ######################################## + swoosh + ######################################## + # host2.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine00.cmh (succeeded) + ######################################## + swoosh + ######################################## + # spine01.cmh (succeeded) + ######################################## + swoosh + ######################################## + # leaf00.cmh (failed) + ######################################## + Traceback (most recent call last): + File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task + r = task._start(host=host, brigade=brigade, dry_run=dry_run) + File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start + r = self.task(self, **self.params) or Result(host) + File "", line 3, in task_that_sometimes_fails + Exception: an uncontrolled exception happened + + ######################################## + # leaf01.cmh (failed) + ######################################## + yikes + + >>> r = b.run(task_that_sometimes_fails, + ... raise_on_error=False) + >>> + +As you can see, regardless of what ``brigade`` had configured to do, the task failed on the first case but didn't on the second one. + +Which one to use +---------------- + +It dependsâ„¢. As a rule of thumb it's probably safer to fail by default and capture errors explicitly. For instance, a continuation you can see an example where we run a task that can change the system and if it fails we try to run a cleanup operation and if it doesn't succeed either we blacklist the host so further tasks are skipped for that host:: + + try: + brigade.run(task_that_attempts_to_change_the_system) + except BrigadeExecutionError as e: + for host in e.failed_hosts.keys(): + r = brigade.filter(name=host).run(task_that_reverts_changes, + raise_on_error=True) + if r.failed: + brigade.data.failed_hosts.add(host) + +In other simpler cases it might be just simpler and completely safe to ignore errors:: + + r = brigade.run(a_task_that_is_safe_if_it_fails) + brigade.run(print_result, + data=result) diff --git a/docs/tutorials/intro/running_tasks_grouping.rst b/docs/tutorials/intro/running_tasks_grouping.rst new file mode 100644 index 00000000..f3330cf7 --- /dev/null +++ b/docs/tutorials/intro/running_tasks_grouping.rst @@ -0,0 +1,227 @@ +Grouping tasks +============== + +Sometimes it is useful to group tasks either for reusability purposes or to speed up the execution (see :doc:`execution model `). Creating groups of tasks is very easy, for instance:: + + def group_of_tasks(task): + task.run(command, + command="echo hi! I am {host} and I am a {host.nos} device") + task.run(command, + command="echo hi! I am a {host[type]}") + +Groups of tasks are called as regular tasks:: + + >>> b = brigade.filter(site="cmh") + >>> result = b.run(group_of_tasks) + >>> + >>> + >>> b.run(print_result, + ... num_workers=1, + ... data=result, + ... vars=["stdout"]) + * host1.cmh ** changed : False ************************************************* + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am host1.cmh and I am a linux device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a host + + * host2.cmh ** changed : False ************************************************* + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am host2.cmh and I am a linux device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a host + + * spine00.cmh ** changed : False *********************************************** + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am spine00.cmh and I am a eos device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a network_device + + * spine01.cmh ** changed : False *********************************************** + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am spine01.cmh and I am a junos device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a network_device + + * leaf00.cmh ** changed : False ************************************************ + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf00.cmh and I am a eos device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a network_device + + * leaf01.cmh ** changed : False ************************************************ + ---- group_of_tasks ** changed : False ---------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + hi! I am leaf01.cmh and I am a junos device + + ---- command ** changed : False ----------------------------------------------- + hi! I am a network_device + +Groups of tasks return for each host a :obj:`brigade.core.task.MultiResult` object which is a list-like object of :obj:`brigade.core.task.Result`. The object will contain the result for each individual task within the group of tasks:: + + >>> result["leaf01.cmh"].__class__ + + >>> result["leaf01.cmh"][0].name + 'group_of_tasks' + >>> result["leaf01.cmh"][1].name + 'command' + >>> result["leaf01.cmh"][1].result + 'hi! I am leaf01.cmh and I am a junos device\n' + +.. note:: Position ``0`` will be the result for the grouping itself while the rest will be the results for the task inside in the same order as defined in there. + +Groups of tasks can also return their own result if needed:: + + >>> from brigade.core.task import Result + >>> + >>> + >>> def group_of_tasks_with_result(task): + ... task.run(command, + ... command="echo hi! I am {host} and I am a {host.nos} device") + ... task.run(command, + ... command="echo hi! I am a {host[type]}") + ... return Result(host=task.host, result="Yippee ki-yay") + ... + >>> result = b.run(group_of_tasks_with_result) + >>> + >>> result["leaf01.cmh"][0].name + 'group_of_tasks_with_result' + >>> result["leaf01.cmh"][0].result + 'Yippee ki-yay' + +Accessing host data +------------------- + +Something interesting about groupings is that you can access host data from them. For instance:: + + >>> def access_host_data(task): + ... if task.host.nos == "eos": + ... task.host["my-new-var"] = "setting a new var for eos" + ... elif task.host.nos == "junos": + ... task.host["my-new-var"] = "setting a new var for junos" + ... + >>> + >>> b.run(access_host_data) + >>> + >>> b.inventory.hosts["leaf00.cmh"]["my-new-var"] + 'setting a new var for eos' + >>> b.inventory.hosts["leaf01.cmh"]["my-new-var"] + 'setting a new var for junos' + +Reusability +----------- + +We mentioned earlier that groups of tasks where also useful for reusability purposes. Let's see it with an example:: + + >>> def count(task, to): + ... task.run(command, + ... command="echo {}".format(list(range(0, to)))) + ... + +Great, we created a super complex task that can count up to an arbitrary number. Let's count to 10:: + + >>> result = b.run(count, + ... to=10) + >>> + >>> + >>> b.run(print_result, + ... num_workers=1, + ... data=result, + ... vars=["stdout"]) + * host1.cmh ** changed : False ************************************************* + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + * host2.cmh ** changed : False ************************************************* + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + * spine00.cmh ** changed : False *********************************************** + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + * spine01.cmh ** changed : False *********************************************** + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + * leaf00.cmh ** changed : False ************************************************ + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + * leaf01.cmh ** changed : False ************************************************ + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + +And now to 20:: + + >>> result = b.run(count, + ... to=20) + >>> + >>> b.run(print_result, + ... num_workers=1, + ... data=result, + ... vars=["stdout"]) + * host1.cmh ** changed : False ************************************************* + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + * host2.cmh ** changed : False ************************************************* + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + * spine00.cmh ** changed : False *********************************************** + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + * spine01.cmh ** changed : False *********************************************** + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + * leaf00.cmh ** changed : False ************************************************ + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + * leaf01.cmh ** changed : False ************************************************ + ---- count ** changed : False ------------------------------------------------- + + ---- command ** changed : False ----------------------------------------------- + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + diff --git a/examples/1_simple_runbooks/get_facts.py b/examples/1_simple_runbooks/get_facts.py index 5defeda5..89bd5a05 100755 --- a/examples/1_simple_runbooks/get_facts.py +++ b/examples/1_simple_runbooks/get_facts.py @@ -1,26 +1,30 @@ #!/usr/bin/env python """ -This is a very simple scripts to get facts and print them on the screen. +This is a very simple runbook to get facts and print them on the screen. """ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import networking, text -brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=True, - config=Config(raise_on_error=False), +from brigade.easy import easy_brigade +from brigade.plugins.functions.text import print_title +from brigade.plugins.tasks.networking import napalm_get +from brigade.plugins.tasks.text import print_result + + +brigade = easy_brigade( + hosts="../hosts.yaml", groups="../groups.yaml", + dry_run=True, + raise_on_error=False, ) +print_title("Getting interfaces and facts") + # select which devices we want to work with filtered = brigade.filter(type="network_device", site="cmh") # we are going to gather "interfaces" and "facts" information with napalm -results = filtered.run(networking.napalm_get, +results = filtered.run(napalm_get, getters=["interfaces", "facts"]) # Let's print the result on screen -filtered.run(text.print_result, +filtered.run(print_result, num_workers=1, # we are printing on screen so we want to do this synchronously data=results) diff --git a/examples/Vagrantfile b/examples/inventory/Vagrantfile similarity index 100% rename from examples/Vagrantfile rename to examples/inventory/Vagrantfile diff --git a/examples/groups.yaml b/examples/inventory/groups.yaml similarity index 100% rename from examples/groups.yaml rename to examples/inventory/groups.yaml diff --git a/examples/hosts.yaml b/examples/inventory/hosts.yaml similarity index 100% rename from examples/hosts.yaml rename to examples/inventory/hosts.yaml diff --git a/examples/network_diagram.graffle b/examples/inventory/network_diagram.graffle similarity index 100% rename from examples/network_diagram.graffle rename to examples/inventory/network_diagram.graffle diff --git a/examples/network_diagram.png b/examples/inventory/network_diagram.png similarity index 100% rename from examples/network_diagram.png rename to examples/inventory/network_diagram.png From 308fd67b7cfe3942a1a668aa18f0b3966a6b8a6d Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 21:57:32 +0100 Subject: [PATCH 24/31] bugfix --- brigade/core/__init__.py | 2 +- brigade/plugins/functions/text/__init__.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index fcb67d95..a02ea1ce 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -204,5 +204,5 @@ def run_task(host, brigade, dry_run, task): logger.error("{}: {}".format(host, tb)) r = Result(host, exception=e, result=tb, failed=True) task.results.append(r) - + r.name = task.name return task.results diff --git a/brigade/plugins/functions/text/__init__.py b/brigade/plugins/functions/text/__init__.py index b7a96615..043171f0 100644 --- a/brigade/plugins/functions/text/__init__.py +++ b/brigade/plugins/functions/text/__init__.py @@ -1,7 +1,4 @@ -from colorama import Fore, Style, init - - -init(autoreset=True) +from colorama import Fore, Style def print_title(title): From 57853e29c4b14f042e6fed6cb5a0932b2b2a7d90 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 21:57:58 +0100 Subject: [PATCH 25/31] allow telling a task to be run on hosts that should be skipped otherwise --- brigade/core/task.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/brigade/core/task.py b/brigade/core/task.py index a65a0343..528e2fff 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -22,17 +22,18 @@ class Task(object): dry_run(``bool``): Populated right before calling the ``task`` """ - def __init__(self, task, name=None, **kwargs): + def __init__(self, task, name=None, skipped=False, **kwargs): self.name = name or task.__name__ self.task = task self.params = kwargs + self.skipped = skipped self.results = MultiResult(self.name) def __repr__(self): return self.name def _start(self, host, brigade, dry_run, sub_task=False): - if host.name in brigade.data.failed_hosts: + if host.name in brigade.data.failed_hosts and not self.skipped: r = Result(host, skipped=True) else: self.host = host From 78248715f951472506774cd886efa416ab951309 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 21:58:22 +0100 Subject: [PATCH 26/31] for consistency with tasks --- brigade/plugins/functions/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/brigade/plugins/functions/__init__.py b/brigade/plugins/functions/__init__.py index 419dd70e..e69de29b 100644 --- a/brigade/plugins/functions/__init__.py +++ b/brigade/plugins/functions/__init__.py @@ -1,6 +0,0 @@ -from . import text - - -__all__ = ( - "text", -) From b9a0d09db4ed7dc9e422f99ae28e6d3cf5a46457 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 22:01:49 +0100 Subject: [PATCH 27/31] allow printing multiresult objects --- brigade/plugins/tasks/text/print_result.py | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/brigade/plugins/tasks/text/print_result.py b/brigade/plugins/tasks/text/print_result.py index 32d19248..60801c59 100644 --- a/brigade/plugins/tasks/text/print_result.py +++ b/brigade/plugins/tasks/text/print_result.py @@ -1,20 +1,23 @@ import pprint -from brigade.core.task import AggregatedResult, Result +from brigade.core.task import AggregatedResult, MultiResult, Result from colorama import Fore, Style, init -init(autoreset=True) +init(autoreset=True, convert=False, strip=False) -def print_result(task, data, vars=None): +def print_result(task, data, vars=None, failed=None, task_id=None): """ Prints on screen the :obj:`brigade.core.task.Result` from a previous task Arguments: data (:obj:`brigade.core.task.Result`): from a previous task vars (list of str): Which attributes you want to print + failed (``bool``): if ``True`` assume the task failed + task_id (``int``): if we have a :obj:`brigade.core.task.MultiResult` print + only task in this position Returns: :obj:`brigade.core.task.Result`: @@ -26,7 +29,12 @@ def print_result(task, data, vars=None): if isinstance(data, AggregatedResult): data = data[task.host.name] - if data.failed: + if task_id is not None: + r = data[task_id] + data = MultiResult(data.name) + data.append(r) + + if data.failed or failed: color = Fore.RED elif data.changed: color = Fore.YELLOW @@ -41,9 +49,10 @@ def print_result(task, data, vars=None): print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg)))) for v in vars: x = getattr(r, v, "") - if r and not isinstance(x, str): - pprint.pprint(x) - elif r: + if x and not isinstance(x, str): + pprint.pprint(x, indent=2) + elif x: print(x) + print() return Result(task.host) From df26d8163a1fe6df5ea7b3299749475b8e10d7fd Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 22:04:41 +0100 Subject: [PATCH 28/31] update docstring --- brigade/core/task.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/brigade/core/task.py b/brigade/core/task.py index 528e2fff..a1af8ad4 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -11,10 +11,16 @@ class Task(object): Arguments: task (callable): function or callable we will be calling + name (``string``): name of task, defaults to ``task.__name__`` + skipped (``bool``): whether to run hosts that should be skipped otherwise or not **kwargs: Parameters that will be passed to the ``task`` Attributes: + task (callable): function or callable we will be calling + name (``string``): name of task, defaults to ``task.__name__`` + skipped (``bool``): whether to run hosts that should be skipped otherwise or not params: Parameters that will be passed to the ``task``. + self.results (:obj:`brigade.core.tasks.MultiResult`): Intermediate results host (:obj:`brigade.core.inventory.Host`): Host we are operating with. Populated right before calling the ``task`` brigade(:obj:`brigade.core.Brigade`): Populated right before calling From 707c9cc760d52cf4d2a8d92c7ed652c8a28bb8c3 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 15 Jan 2018 22:05:28 +0100 Subject: [PATCH 29/31] updated examples --- docs/_static/css/custom.css | 12 + docs/conf.py | 5 +- docs/howto/basic-napalm-getters.rst | 146 --- .../from_runbooks_to_complex_tooling.rst | 11 + docs/howto/index.rst | 2 +- docs/howto/simple_runbooks/backup.ipynb | 1 + docs/howto/simple_runbooks/configure.ipynb | 1 + docs/howto/simple_runbooks/get_facts.ipynb | 1 + docs/howto/simple_runbooks/index.rst | 11 + docs/howto/simple_runbooks/rollback.ipynb | 1 + docs/howto/simple_runbooks/validate.ipynb | 1 + docs/howto/simple_tooling/backup.ipynb | 1 + docs/howto/simple_tooling/configure.ipynb | 1 + docs/howto/simple_tooling/get_facts.ipynb | 1 + docs/howto/simple_tooling/index.rst | 13 + docs/howto/simple_tooling/rollback.ipynb | 1 + docs/howto/simple_tooling/validate.ipynb | 1 + docs/requirements.txt | 2 + examples/1_simple_runbooks/backup.ipynb | 557 +++++++++ examples/1_simple_runbooks/backup.py | 38 +- examples/1_simple_runbooks/configure.ipynb | 673 +++++++++++ examples/1_simple_runbooks/configure.py | 56 +- examples/1_simple_runbooks/get_facts.ipynb | 662 ++++++++++ examples/1_simple_runbooks/get_facts.py | 26 +- examples/1_simple_runbooks/rollback.ipynb | 438 +++++++ examples/1_simple_runbooks/rollback.py | 47 + examples/1_simple_runbooks/validate.ipynb | 689 +++++++++++ examples/1_simple_runbooks/validate.py | 33 +- examples/2_simple_tooling/backup.ipynb | 1062 +++++++++++++++++ examples/2_simple_tooling/backup.py | 57 +- examples/2_simple_tooling/configure.ipynb | 747 ++++++++++++ examples/2_simple_tooling/configure.py | 59 +- examples/2_simple_tooling/get_facts.ipynb | 306 +++++ examples/2_simple_tooling/get_facts.py | 25 +- examples/2_simple_tooling/rollback.ipynb | 411 +++++++ examples/2_simple_tooling/rollback.py | 56 + examples/2_simple_tooling/validate.ipynb | 404 +++++++ examples/2_simple_tooling/validate.py | 35 +- examples/highlighter.py | 34 + 39 files changed, 6362 insertions(+), 265 deletions(-) create mode 100644 docs/_static/css/custom.css delete mode 100644 docs/howto/basic-napalm-getters.rst create mode 100644 docs/howto/from_runbooks_to_complex_tooling.rst create mode 120000 docs/howto/simple_runbooks/backup.ipynb create mode 120000 docs/howto/simple_runbooks/configure.ipynb create mode 120000 docs/howto/simple_runbooks/get_facts.ipynb create mode 100644 docs/howto/simple_runbooks/index.rst create mode 120000 docs/howto/simple_runbooks/rollback.ipynb create mode 120000 docs/howto/simple_runbooks/validate.ipynb create mode 120000 docs/howto/simple_tooling/backup.ipynb create mode 120000 docs/howto/simple_tooling/configure.ipynb create mode 120000 docs/howto/simple_tooling/get_facts.ipynb create mode 100644 docs/howto/simple_tooling/index.rst create mode 120000 docs/howto/simple_tooling/rollback.ipynb create mode 120000 docs/howto/simple_tooling/validate.ipynb create mode 100644 examples/1_simple_runbooks/backup.ipynb create mode 100644 examples/1_simple_runbooks/configure.ipynb create mode 100644 examples/1_simple_runbooks/get_facts.ipynb create mode 100644 examples/1_simple_runbooks/rollback.ipynb create mode 100755 examples/1_simple_runbooks/rollback.py create mode 100644 examples/1_simple_runbooks/validate.ipynb create mode 100644 examples/2_simple_tooling/backup.ipynb create mode 100644 examples/2_simple_tooling/configure.ipynb create mode 100644 examples/2_simple_tooling/get_facts.ipynb create mode 100644 examples/2_simple_tooling/rollback.ipynb create mode 100755 examples/2_simple_tooling/rollback.py create mode 100644 examples/2_simple_tooling/validate.ipynb create mode 100644 examples/highlighter.py diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 00000000..2da34a19 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,12 @@ +div.pygments pre { + font-size: 0.8em; + padding: 0.5em 0.5em 0.5em 0.5em; +} + +span.lineno { + color: gray; +} + +span.lineno::after { + content: "|" +} diff --git a/docs/conf.py b/docs/conf.py index ba2321d7..77bde96b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,7 +38,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'nbsphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -101,7 +101,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] +html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -190,6 +190,7 @@ def build_configuration_parameters(app): def setup(app): """Map methods to states of the documentation build.""" app.connect('builder-inited', build_configuration_parameters) + app.add_stylesheet('css/custom.css') build_configuration_parameters(None) diff --git a/docs/howto/basic-napalm-getters.rst b/docs/howto/basic-napalm-getters.rst deleted file mode 100644 index dbd65e6e..00000000 --- a/docs/howto/basic-napalm-getters.rst +++ /dev/null @@ -1,146 +0,0 @@ -Gathering information with NAPALM -################################# - -Inventory -========= - -Let's start by seeing how to work with the inventory. Let's assume the following files: - -* Hosts file: - -.. literalinclude:: ../../examples/inventory/hosts.yaml - :name: hosts.yaml - :language: yaml - -* Groups file: - -.. literalinclude:: ../../examples/inventory/groups.yaml - :name: groups.yaml - :language: yaml - -We can instantiate Brigade as follows:: - - >>> from brigade.core import Brigade - >>> from brigade.plugins.inventory.simple import SimpleInventory - >>> brigade = Brigade( - ... inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - ... dry_run=True) - >>> brigade.inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh', 'host1.bma', 'host2.bma', 'switch00.bma', 'switch01.bma']) - >>> brigade.inventory.groups.keys() - dict_keys(['all', 'bma-leaf', 'bma-host', 'bma', 'cmh-leaf', 'cmh-host', 'cmh']) - -As you can see instantiating brigade and providing inventory information is very easy. Now let's see how we can filter hosts. This will be useful when we want to apply certain tasks to only certain devices:: - - >>> brigade.filter(site="cmh").inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh']) - >>> brigade.filter(site="cmh", role="leaf").inventory.hosts.keys() - dict_keys(['switch00.cmh', 'switch01.cmh']) - -You can basically filter by any attribute the device has. The filter is also cumulative:: - - >>> cmh = brigade.filter(site="cmh") - >>> cmh.inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh']) - >>> cmh.filter(role="leaf").inventory.hosts.keys() - dict_keys(['switch00.cmh', 'switch01.cmh']) - -Data -==== - -Now let's see how to access data. Let's start by grabbing a host:: - - >>> host = brigade.inventory.hosts["switch00.cmh"] - -Now, you can access host data either via the host itself, as it behaves like a dict, or via it's ``data`` attribute. The difference is that if access data via the host itself the information will be resolved and data inherited by parent groups will be accessible while if you access the data via the ``data`` attribute only data belonging to the host will be accessible. Let's see a few examples, refer to the files on top of this document for reference:: - - >>> host["nos"] - 'eos' - >>> host.data["nos"] - 'eos' - >>> host["domain"] - 'acme.com' - >>> host.domain["domain"] - Traceback (most recent call last): - File "", line 1, in - AttributeError: 'Host' object has no attribute 'domain' - -You can access the parent group via the ``group`` attribute and :obj:`brigade.core.inventory.Group` behave in the same exact way as :obj:`brigade.core.inventory.Host`:: - - >>> host.group - Group: cmh-leaf - >>> host.group["domain"] - 'acme.com' - >>> host.group.data["domain"] - Traceback (most recent call last): - File "", line 1, in - KeyError: 'domain' - -Tasks -===== - -Now we know how to deal with the inventory let's try to use plugin to gather device information:: - - >>> from brigade.plugins import tasks - >>> cmh_leaf = brigade.filter(site="cmh", role="leaf") - >>> result = cmh_leaf.run(task=tasks.napalm_get_facts, - ... facts="facts") - >>> print(result) - {'switch00.cmh': {'result': {'hostname': 'switch00.cmh', 'fqdn': 'switch00.cmh.cmh.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83187, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']}}, 'switch01.cmh': {'result': {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.cmh', 'fqdn': 'switch01.cmh.cmh.acme.com', 'uptime': 83084, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']}}} - -You can also group multiple tasks into a single block:: - - >>> def get_info(task): - ... # Grouping multiple tasks that go together - ... r = tasks.napalm_get_facts(task, "facts") - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... r = tasks.napalm_get_facts(task, "interfaces") - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... print() - ... - >>> cmh_leaf.run(task=get_info) - switch00.cmh - ============ - {'hostname': 'switch00.bma', 'fqdn': 'switch00.bma.bma.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83424, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']} - switch00.cmh - ============ - {'Ethernet2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 1511034159.0399787, 'speed': 0, 'mac_address': '08:00:27:AB:42:B6'}, 'Management1': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 1511033376.7964435, 'speed': 1000, 'mac_address': '08:00:27:47:87:83'}, 'Ethernet1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 1511033362.0302556, 'speed': 0, 'mac_address': '08:00:27:2D:F4:5A'}} - - switch01.cmh - ============ - {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.bma', 'fqdn': 'switch01.bma.bma.acme.com', 'uptime': 83320, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']} - switch01.cmh - ============ - {'ge-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83272.0, 'mac_address': '08:00:27:AA:8C:76', 'speed': 1000}, 'gr-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ip-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'lsq-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83273.0, 'mac_address': 'None', 'speed': -1}, 'lt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '02:96:14:8C:76:B3', 'speed': 800}, 'mt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'sp-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83273.0, 'mac_address': 'Unspecified', 'speed': 800}, 'ge-0/0/1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 83272.0, 'mac_address': '08:00:27:FB:F0:FC', 'speed': 1000}, 'ge-0/0/2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 82560.0, 'mac_address': '08:00:27:32:60:54', 'speed': 1000}, '.local.': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'dsc': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'gre': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'ipip': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'irb': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '4C:96:14:8C:76:B0', 'speed': -1}, 'lo0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'lsi': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'mtun': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pimd': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pime': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pp0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'ppd0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ppe0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'st0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'tap': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'vlan': {'is_up': False, 'is_enabled': True, 'description': '', 'last_flapped': 83282.0, 'mac_address': '00:00:00:00:00:00', 'speed': 1000}} - -Or even reuse:: - - >>> def get_facts(task, facts): - ... # variable "facts" will let us reuse this for multiple purposes - ... r = tasks.napalm_get_facts(task, facts) - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... print() - ... - >>> cmh_leaf.run(task=get_facts, facts="facts") - switch00.cmh - ============ - {'hostname': 'switch00.bma', 'fqdn': 'switch00.bma.bma.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83534, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']} - - switch01.cmh - ============ - {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.bma', 'fqdn': 'switch01.bma.bma.acme.com', 'uptime': 83431, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']} - - >>> cmh_leaf.run(task=get_facts, facts="interfaces") - switch00.cmh - ============ - {'Ethernet2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 1511034159.0400095, 'speed': 0, 'mac_address': '08:00:27:AB:42:B6'}, 'Management1': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 1511033376.7963786, 'speed': 1000, 'mac_address': '08:00:27:47:87:83'}, 'Ethernet1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 1511033362.0302918, 'speed': 0, 'mac_address': '08:00:27:2D:F4:5A'}} - - switch01.cmh - ============ - {'ge-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83387.0, 'mac_address': '08:00:27:AA:8C:76', 'speed': 1000}, 'gr-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ip-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'lsq-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83388.0, 'mac_address': 'None', 'speed': -1}, 'lt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '02:96:14:8C:76:B3', 'speed': 800}, 'mt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'sp-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83388.0, 'mac_address': 'Unspecified', 'speed': 800}, 'ge-0/0/1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 83387.0, 'mac_address': '08:00:27:FB:F0:FC', 'speed': 1000}, 'ge-0/0/2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 82675.0, 'mac_address': '08:00:27:32:60:54', 'speed': 1000}, '.local.': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'dsc': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'gre': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'ipip': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'irb': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '4C:96:14:8C:76:B0', 'speed': -1}, 'lo0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'lsi': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'mtun': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pimd': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pime': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pp0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'ppd0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ppe0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'st0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'tap': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'vlan': {'is_up': False, 'is_enabled': True, 'description': '', 'last_flapped': 83397.0, 'mac_address': '00:00:00:00:00:00', 'speed': 1000}} diff --git a/docs/howto/from_runbooks_to_complex_tooling.rst b/docs/howto/from_runbooks_to_complex_tooling.rst new file mode 100644 index 00000000..3f56fe61 --- /dev/null +++ b/docs/howto/from_runbooks_to_complex_tooling.rst @@ -0,0 +1,11 @@ +From Runbooks to Advanced Tooling +================================= + +In this section we are going to build advanced tooling in a series of baby steps. We will start writing very simple runbooks, then we will slightly rewrite those runbooks to turn them into flexible cli tools. Once we are done with that we will turn those isolated cli tools into an advanced tool that can accommodate different workflows. + +.. toctree:: + :maxdepth: 1 + :glob: + + simple_runbooks/index + simple_tooling/index diff --git a/docs/howto/index.rst b/docs/howto/index.rst index 69843994..dfadc113 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -5,4 +5,4 @@ How to use Brigade :maxdepth: 1 :glob: - * \ No newline at end of file + * diff --git a/docs/howto/simple_runbooks/backup.ipynb b/docs/howto/simple_runbooks/backup.ipynb new file mode 120000 index 00000000..f6561d19 --- /dev/null +++ b/docs/howto/simple_runbooks/backup.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/backup.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/configure.ipynb b/docs/howto/simple_runbooks/configure.ipynb new file mode 120000 index 00000000..9fc2fc70 --- /dev/null +++ b/docs/howto/simple_runbooks/configure.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/configure.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/get_facts.ipynb b/docs/howto/simple_runbooks/get_facts.ipynb new file mode 120000 index 00000000..cdf882f1 --- /dev/null +++ b/docs/howto/simple_runbooks/get_facts.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/get_facts.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/index.rst b/docs/howto/simple_runbooks/index.rst new file mode 100644 index 00000000..f63c4e42 --- /dev/null +++ b/docs/howto/simple_runbooks/index.rst @@ -0,0 +1,11 @@ +Simple Runbooks +=============== + +In this series we are going to build a few simple runbooks to do various tasks on the network. Each runbook is going to do one specific tasks and is going to make certain assumptions to simplify the logic as possible; like which devices are involved, where is the data located, etc. In following series we will build on these runbooks to build more flexible and complex tooling. + +.. toctree:: + :maxdepth: 1 + :glob: + + * + diff --git a/docs/howto/simple_runbooks/rollback.ipynb b/docs/howto/simple_runbooks/rollback.ipynb new file mode 120000 index 00000000..4081ad20 --- /dev/null +++ b/docs/howto/simple_runbooks/rollback.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/rollback.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/validate.ipynb b/docs/howto/simple_runbooks/validate.ipynb new file mode 120000 index 00000000..91119d86 --- /dev/null +++ b/docs/howto/simple_runbooks/validate.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/validate.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/backup.ipynb b/docs/howto/simple_tooling/backup.ipynb new file mode 120000 index 00000000..4cd21f71 --- /dev/null +++ b/docs/howto/simple_tooling/backup.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/backup.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/configure.ipynb b/docs/howto/simple_tooling/configure.ipynb new file mode 120000 index 00000000..71c86d66 --- /dev/null +++ b/docs/howto/simple_tooling/configure.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/configure.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/get_facts.ipynb b/docs/howto/simple_tooling/get_facts.ipynb new file mode 120000 index 00000000..450584f1 --- /dev/null +++ b/docs/howto/simple_tooling/get_facts.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/get_facts.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/index.rst b/docs/howto/simple_tooling/index.rst new file mode 100644 index 00000000..4a338209 --- /dev/null +++ b/docs/howto/simple_tooling/index.rst @@ -0,0 +1,13 @@ +Simple Tooling +============== + +In this series we are going to build on top of the runbooks we built on :doc:`the previous section <../simple_runbooks/index>` and build more versatile tooling. Most tools will be not that very different from its equivalent runbook so you should be able to look at them side by side and realize how easy it was to build a cli tool from a previous runbook thanks to the fact that brigade is a native python framework and integrates natively with other frameworks like `click `. + + +.. toctree:: + :maxdepth: 1 + :glob: + + * + + diff --git a/docs/howto/simple_tooling/rollback.ipynb b/docs/howto/simple_tooling/rollback.ipynb new file mode 120000 index 00000000..bd29b917 --- /dev/null +++ b/docs/howto/simple_tooling/rollback.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/rollback.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/validate.ipynb b/docs/howto/simple_tooling/validate.ipynb new file mode 120000 index 00000000..e7b38cfd --- /dev/null +++ b/docs/howto/simple_tooling/validate.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/validate.ipynb \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 6fab96cb..e28da7c4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,8 @@ sphinx sphinx_rtd_theme sphinxcontrib-napoleon +jupyter +nbsphinx -r ../requirements.txt future # jtextfsm diff --git a/examples/1_simple_runbooks/backup.ipynb b/examples/1_simple_runbooks/backup.ipynb new file mode 100644 index 00000000..8f5956ec --- /dev/null +++ b/examples/1_simple_runbooks/backup.ipynb @@ -0,0 +1,557 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Backup\n", + "\n", + "This runbook is going to download the configuration of the devices and save it under `./backup/$hostname`. It also reports changes as we will a continuation.\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "

 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Runbook that downloads the configuration from the devices and\n",
+       " 4 stores them on disk.\n",
+       " 5 """\n",
+       " 6 from brigade.easy import easy_brigade\n",
+       " 7 from brigade.plugins.tasks import files, networking, text\n",
+       " 8 \n",
+       " 9 \n",
+       "10 def backup(task):\n",
+       "11     """\n",
+       "12     This function groups two tasks:\n",
+       "13         1. Download configuration from the device\n",
+       "14         2. Store to disk\n",
+       "15     """\n",
+       "16     result = task.run(networking.napalm_get,\n",
+       "17                       name="Gathering configuration",\n",
+       "18                       getters="config")\n",
+       "19 \n",
+       "20     task.run(files.write,\n",
+       "21              name="Saving Configuration to disk",\n",
+       "22              content=result.result["config"]["running"],\n",
+       "23              filename="./backups/{}".format(task.host))\n",
+       "24 \n",
+       "25 \n",
+       "26 brg = easy_brigade(\n",
+       "27         host_file="../inventory/hosts.yaml",\n",
+       "28         group_file="../inventory/groups.yaml",\n",
+       "29         dry_run=False,\n",
+       "30         raise_on_error=True,\n",
+       "31 )\n",
+       "32 \n",
+       "33 # select which devices we want to work with\n",
+       "34 filtered = brg.filter(type="network_device", site="cmh")\n",
+       "35 \n",
+       "36 # Run the ``backup`` function that groups the tasks to\n",
+       "37 # download/store devices' configuration\n",
+       "38 results = filtered.run(backup,\n",
+       "39                        name="Backing up configurations")\n",
+       "40 \n",
+       "41 # Let's print the result on screen\n",
+       "42 filtered.run(text.print_result,\n",
+       "43              num_workers=1,  # task should be done synchronously\n",
+       "44              data=results,\n",
+       "45              task_id=-1,  # we only want to print the last task\n",
+       "46              )\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file backup.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Let's run the command for the first time (note we are cleaning first `./backups/` folder to pretend each run of the following cell is the first one):" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- ./backups/spine00.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,34 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: localhost (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + "+!\n", + "+interface Ethernet1\n", + "+!\n", + "+interface Ethernet2\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+no ip routing\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- ./backups/spine01.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,70 @@\n", + "\n", + "+\n", + "+## Last commit: 2018-01-14 14:33:48 UTC by vagrant\n", + "+version 12.1X47-D20.7;\n", + "+system {\n", + "+ host-name vsrx;\n", + "+ root-authentication {\n", + "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", + "+ }\n", + "+ login {\n", + "+ user vagrant {\n", + "+ uid 2000;\n", + "+ class super-user;\n", + "+ authentication {\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ services {\n", + "+ ssh {\n", + "+ root-login allow;\n", + "+ }\n", + "+ netconf {\n", + "+ ssh;\n", + "+ }\n", + "+ web-management {\n", + "+ http {\n", + "+ interface ge-0/0/0.0;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ syslog {\n", + "+ user * {\n", + "+ any emergency;\n", + "+ }\n", + "+ file messages {\n", + "+ any any;\n", + "+ authorization info;\n", + "+ }\n", + "+ file interactive-commands {\n", + "+ interactive-commands any;\n", + "+ }\n", + "+ }\n", + "+ license {\n", + "+ autoupdate {\n", + "+ url https://ae1.juniper.net/junos/key_retrieval;\n", + "+ }\n", + "+ }\n", + "+}\n", + "+interfaces {\n", + "+ ge-0/0/0 {\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ dhcp;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+security {\n", + "+ forwarding-options {\n", + "+ family {\n", + "+ inet6 {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ mpls {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- ./backups/leaf00.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,34 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: localhost (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + "+!\n", + "+interface Ethernet1\n", + "+!\n", + "+interface Ethernet2\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+no ip routing\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- ./backups/leaf01.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,70 @@\n", + "\n", + "+\n", + "+## Last commit: 2018-01-14 14:33:48 UTC by vagrant\n", + "+version 12.1X47-D20.7;\n", + "+system {\n", + "+ host-name vsrx;\n", + "+ root-authentication {\n", + "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\";\n", + "+ }\n", + "+ login {\n", + "+ user vagrant {\n", + "+ uid 2000;\n", + "+ class super-user;\n", + "+ authentication {\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ services {\n", + "+ ssh {\n", + "+ root-login allow;\n", + "+ }\n", + "+ netconf {\n", + "+ ssh;\n", + "+ }\n", + "+ web-management {\n", + "+ http {\n", + "+ interface ge-0/0/0.0;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ syslog {\n", + "+ user * {\n", + "+ any emergency;\n", + "+ }\n", + "+ file messages {\n", + "+ any any;\n", + "+ authorization info;\n", + "+ }\n", + "+ file interactive-commands {\n", + "+ interactive-commands any;\n", + "+ }\n", + "+ }\n", + "+ license {\n", + "+ autoupdate {\n", + "+ url https://ae1.juniper.net/junos/key_retrieval;\n", + "+ }\n", + "+ }\n", + "+}\n", + "+interfaces {\n", + "+ ge-0/0/0 {\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ dhcp;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+security {\n", + "+ forwarding-options {\n", + "+ family {\n", + "+ inet6 {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ mpls {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%rm backups/* > /dev/null\n", + "%run backup.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's run it again to see how ``brigade`` detects no changes in the backup:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run backup.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's change the device's hostname and run the backup tool again:\n", + "\n", + " localhost(config)#hostname blah\n", + " blah(config)# end" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- ./backups/spine00.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -1,5 +1,5 @@\n", + "\n", + " ! Command: show running-config\n", + "-! device: localhost (vEOS, EOS-4.17.5M)\n", + "+! device: blah (vEOS, EOS-4.17.5M)\n", + " !\n", + " ! boot system flash:/vEOS-lab.swi\n", + " !\n", + "@@ -8,6 +8,8 @@\n", + "\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname blah\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run backup.py" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/1_simple_runbooks/backup.py b/examples/1_simple_runbooks/backup.py index 84f247f1..baec0417 100755 --- a/examples/1_simple_runbooks/backup.py +++ b/examples/1_simple_runbooks/backup.py @@ -1,28 +1,46 @@ #!/usr/bin/env python +""" +Runbook that downloads the configuration from the devices and +stores them on disk. +""" from brigade.easy import easy_brigade from brigade.plugins.tasks import files, networking, text def backup(task): + """ + This function groups two tasks: + 1. Download configuration from the device + 2. Store to disk + """ result = task.run(networking.napalm_get, + name="Gathering configuration", getters="config") - return task.run(files.write, - content=result.result["config"]["running"], - filename="./backups/{}".format(task.host)) + task.run(files.write, + name="Saving Configuration to disk", + content=result.result["config"]["running"], + filename="./backups/{}".format(task.host)) -brigade = easy_brigade( - hosts="../hosts.yaml", groups="../groups.yaml", +brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", dry_run=False, - raise_on_error=False, + raise_on_error=True, ) # select which devices we want to work with -filtered = brigade.filter(type="network_device", site="cmh") -results = filtered.run(backup) +filtered = brg.filter(type="network_device", site="cmh") + +# Run the ``backup`` function that groups the tasks to +# download/store devices' configuration +results = filtered.run(backup, + name="Backing up configurations") # Let's print the result on screen filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + ) diff --git a/examples/1_simple_runbooks/configure.ipynb b/examples/1_simple_runbooks/configure.ipynb new file mode 100644 index 00000000..2f409b64 --- /dev/null +++ b/examples/1_simple_runbooks/configure.ipynb @@ -0,0 +1,673 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Configure\n", + "\n", + "This is a runbook to configure the network. To do so we are going to load first some data from the directory `../extra_data/` and then a bunch of templates to generate, based on that extra data, the configuration for the devices.\n", + "\n", + "## Extra data\n", + "\n", + "Let's first look at the extra data we are going to use:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../extra_data/leaf00.cmh:\r\n", + "l3.yaml\r\n", + "\r\n", + "../extra_data/leaf01.cmh:\r\n", + "l3.yaml\r\n", + "\r\n", + "../extra_data/spine00.cmh:\r\n", + "l3.yaml\r\n", + "\r\n", + "../extra_data/spine01.cmh:\r\n", + "l3.yaml\r\n" + ] + } + ], + "source": [ + "%ls ../extra_data/*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's look at one of the files for reference:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---\r\n", + "interfaces:\r\n", + " Ethernet1:\r\n", + " connects_to: spine00.cmh\r\n", + " ipv4: 10.0.0.1/31\r\n", + " enabled: false\r\n", + " Ethernet2:\r\n", + " connects_to: spine01.cmh\r\n", + " ipv4: 10.0.1.1/31\r\n", + " enabled: true\r\n", + "\r\n", + "sessions:\r\n", + " - ipv4: 10.0.0.0\r\n", + " peer_as: 65000\r\n", + " - ipv4: 10.0.1.0\r\n", + " peer_as: 65000\r\n" + ] + } + ], + "source": [ + "% cat ../extra_data/leaf00.cmh/l3.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Templates\n", + "\n", + "To configure the network we will transform the data we saw before into actual configurationusing jinja2 templates. The templates are:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../templates/eos:\r\n", + "base.j2 interfaces.j2 leaf.j2 routing.j2 spine.j2\r\n", + "\r\n", + "../templates/junos:\r\n", + "base.j2 interfaces.j2 leaf.j2 routing.j2 spine.j2\r\n" + ] + } + ], + "source": [ + "%ls ../templates/*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As an example, let's look how the ``interfaces.j2`` template will consume the extra data we saw before to configure the interfaces:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{% for interface, data in l3.interfaces.items() %}\r\n", + "interface {{ interface }}\r\n", + " no switchport\r\n", + " ip address {{ data.ipv4 }}\r\n", + " description link to {{ data.connects_to }}\r\n", + " {{ \"no\" if data.enabled else \"\" }} shutdown\r\n", + "{% endfor %}\r\n", + "\r\n" + ] + } + ], + "source": [ + "%cat ../templates/eos/interfaces.j2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code\n", + "\n", + "Now let's look at the code that will sticth everything together:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Runbook to configure datacenter\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.functions.text import print_title\n",
+       " 7 from brigade.plugins.tasks import data, networking, text\n",
+       " 8 \n",
+       " 9 \n",
+       "10 def configure(task):\n",
+       "11     """\n",
+       "12     This function groups all the tasks needed to configure the\n",
+       "13     network:\n",
+       "14 \n",
+       "15         1. Loading extra data\n",
+       "16         2. Templates to build configuration\n",
+       "17         3. Deploy configuration on the device\n",
+       "18     """\n",
+       "19     r = task.run(text.template_file,\n",
+       "20                  name="Base Configuration",\n",
+       "21                  template="base.j2",\n",
+       "22                  path="../templates/{brigade_nos}")\n",
+       "23     # r.result holds the result of rendering the template\n",
+       "24     # we store in the host itself so we can keep updating\n",
+       "25     # it as we render other templates\n",
+       "26     task.host["config"] = r.result\n",
+       "27 \n",
+       "28     r = task.run(data.load_yaml,\n",
+       "29                  name="Loading extra data",\n",
+       "30                  file="../extra_data/{host}/l3.yaml")\n",
+       "31     # r.result holds the data contained in the yaml files\n",
+       "32     # we load the data inside the host itself for further use\n",
+       "33     task.host["l3"] = r.result\n",
+       "34 \n",
+       "35     r = task.run(text.template_file,\n",
+       "36                  name="Interfaces Configuration",\n",
+       "37                  template="interfaces.j2",\n",
+       "38                  path="../templates/{brigade_nos}")\n",
+       "39     # we update our hosts' config\n",
+       "40     task.host["config"] += r.result\n",
+       "41 \n",
+       "42     r = task.run(text.template_file,\n",
+       "43                  name="Routing Configuration",\n",
+       "44                  template="routing.j2",\n",
+       "45                  path="../templates/{brigade_nos}")\n",
+       "46     # we update our hosts' config\n",
+       "47     task.host["config"] += r.result\n",
+       "48 \n",
+       "49     r = task.run(text.template_file,\n",
+       "50                  name="Role-specific Configuration",\n",
+       "51                  template="{role}.j2",\n",
+       "52                  path="../templates/{brigade_nos}")\n",
+       "53     # we update our hosts' config\n",
+       "54     task.host["config"] += r.result\n",
+       "55 \n",
+       "56     task.run(networking.napalm_configure,\n",
+       "57              name="Loading Configuration on the device",\n",
+       "58              replace=False,\n",
+       "59              configuration=task.host["config"])\n",
+       "60 \n",
+       "61 \n",
+       "62 brg = easy_brigade(\n",
+       "63         host_file="../inventory/hosts.yaml",\n",
+       "64         group_file="../inventory/groups.yaml",\n",
+       "65         dry_run=False,\n",
+       "66         raise_on_error=True,\n",
+       "67 )\n",
+       "68 \n",
+       "69 \n",
+       "70 # select which devices we want to work with\n",
+       "71 filtered = brg.filter(type="network_device", site="cmh")\n",
+       "72 \n",
+       "73 results = filtered.run(task=configure)\n",
+       "74 \n",
+       "75 print_title("Playbook to configure the network")\n",
+       "76 filtered.run(text.print_result,\n",
+       "77              name="Configure device",\n",
+       "78              num_workers=1,  # task should be done synchronously\n",
+       "79              data=results,\n",
+       "80              task_id=-1,  # we only want to print the last task\n",
+       "81              )\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file configure.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Finally let's see everything in action:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,7 +8,8 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "-hostname localhost\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + " !\n", + " spanning-tree mode mstp\n", + " !\n", + "@@ -20,13 +21,28 @@\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + " !\n", + " interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65000;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,6 +8,9 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + " spanning-tree mode mstp\n", + " !\n", + " aaa authorization exec default local\n", + "@@ -17,14 +20,36 @@\n", + " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + " !\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + " interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to spine00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.0.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to spine01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65101;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.0.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ neighbor 10.0.1.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\n", + "+ vlans {\n", + "+ backend {\n", + "+ vlan-id 200;\n", + "+ }\n", + "+ frontend {\n", + "+ vlan-id 100;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The tool also detects unwanted changes and corrects them. For instance, let's change the hostname manually:\n", + "\n", + " spine00.cmh((config)#hostname localhost\n", + " localhost(config)#\n", + "\n", + "And run the runbook again:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,7 +8,7 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "-hostname localhost\n", + "+hostname spine00.cmh\n", + " ip domain-name cmh.acme.com\n", + " !\n", + " spanning-tree mode mstp\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/1_simple_runbooks/configure.py b/examples/1_simple_runbooks/configure.py index 9f44d08a..d120a723 100755 --- a/examples/1_simple_runbooks/configure.py +++ b/examples/1_simple_runbooks/configure.py @@ -1,53 +1,81 @@ #!/usr/bin/env python """ -In this example we write a CLI tool with brigade and click to deploy configuration. +Runbook to configure datacenter """ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade +from brigade.plugins.functions.text import print_title from brigade.plugins.tasks import data, networking, text def configure(task): + """ + This function groups all the tasks needed to configure the + network: + + 1. Loading extra data + 2. Templates to build configuration + 3. Deploy configuration on the device + """ r = task.run(text.template_file, + name="Base Configuration", template="base.j2", path="../templates/{brigade_nos}") + # r.result holds the result of rendering the template + # we store in the host itself so we can keep updating + # it as we render other templates task.host["config"] = r.result r = task.run(data.load_yaml, + name="Loading extra data", file="../extra_data/{host}/l3.yaml") + # r.result holds the data contained in the yaml files + # we load the data inside the host itself for further use task.host["l3"] = r.result r = task.run(text.template_file, + name="Interfaces Configuration", template="interfaces.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result r = task.run(text.template_file, + name="Routing Configuration", template="routing.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result r = task.run(text.template_file, + name="Role-specific Configuration", template="{role}.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result - return task.run(networking.napalm_configure, - replace=False, - configuration=task.host["config"]) + task.run(networking.napalm_configure, + name="Loading Configuration on the device", + replace=False, + configuration=task.host["config"]) -brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=False, - config=Config(raise_on_error=False), +brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=True, ) -filtered = brigade.filter(type="network_device", site="cmh") + +# select which devices we want to work with +filtered = brg.filter(type="network_device", site="cmh") results = filtered.run(task=configure) +print_title("Playbook to configure the network") filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) + name="Configure device", + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + ) diff --git a/examples/1_simple_runbooks/get_facts.ipynb b/examples/1_simple_runbooks/get_facts.ipynb new file mode 100644 index 00000000..920b6eba --- /dev/null +++ b/examples/1_simple_runbooks/get_facts.ipynb @@ -0,0 +1,662 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "hide_input": true + }, + "source": [ + "# Get Facts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Following runbook will connect to devices in the site \"cmh\" and gather information about basic facts and interfaces.\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Very simple runbook to get facts and print them on the screen.\n",
+       " 4 """\n",
+       " 5 \n",
+       " 6 from brigade.easy import easy_brigade\n",
+       " 7 from brigade.plugins.functions.text import print_title\n",
+       " 8 from brigade.plugins.tasks import networking, text\n",
+       " 9 \n",
+       "10 \n",
+       "11 brg = easy_brigade(\n",
+       "12         host_file="../inventory/hosts.yaml",\n",
+       "13         group_file="../inventory/groups.yaml",\n",
+       "14         dry_run=False,\n",
+       "15         raise_on_error=False,\n",
+       "16 )\n",
+       "17 \n",
+       "18 print_title("Getting interfaces and facts")\n",
+       "19 \n",
+       "20 # select which devices we want to work with\n",
+       "21 filtered = brg.filter(type="network_device", site="cmh")\n",
+       "22 \n",
+       "23 # we are going to gather "interfaces" and "facts"\n",
+       "24 # information with napalm\n",
+       "25 results = filtered.run(networking.napalm_get,\n",
+       "26                        getters=["interfaces", "facts"])\n",
+       "27 \n",
+       "28 # Let's print the result on screen\n",
+       "29 filtered.run(text.print_result,\n",
+       "30              num_workers=1,  # task should be done synchronously\n",
+       "31              data=results,\n",
+       "32              )\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file get_facts.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Let's run it:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32m**** Getting interfaces and facts **********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine00.cmh.cmh.acme.com'\u001b[0m,\n", + " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", + " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", + " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", + " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'uptime'\u001b[0m: \u001b[0m76742\u001b[0m,\n", + " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516010990.2331386\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939602.295958\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939616.808321\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine01.cmh.cmh.acme.com'\u001b[0m,\n", + " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m'ge-0/0/0'\u001b[0m,\n", + " \u001b[0m'gr-0/0/0'\u001b[0m,\n", + " \u001b[0m'ip-0/0/0'\u001b[0m,\n", + " \u001b[0m'lsq-0/0/0'\u001b[0m,\n", + " \u001b[0m'lt-0/0/0'\u001b[0m,\n", + " \u001b[0m'mt-0/0/0'\u001b[0m,\n", + " \u001b[0m'sp-0/0/0'\u001b[0m,\n", + " \u001b[0m'ge-0/0/1'\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m,\n", + " \u001b[0m'.local.'\u001b[0m,\n", + " \u001b[0m'dsc'\u001b[0m,\n", + " \u001b[0m'gre'\u001b[0m,\n", + " \u001b[0m'ipip'\u001b[0m,\n", + " \u001b[0m'irb'\u001b[0m,\n", + " \u001b[0m'lo0'\u001b[0m,\n", + " \u001b[0m'lsi'\u001b[0m,\n", + " \u001b[0m'mtun'\u001b[0m,\n", + " \u001b[0m'pimd'\u001b[0m,\n", + " \u001b[0m'pime'\u001b[0m,\n", + " \u001b[0m'pp0'\u001b[0m,\n", + " \u001b[0m'ppd0'\u001b[0m,\n", + " \u001b[0m'ppe0'\u001b[0m,\n", + " \u001b[0m'st0'\u001b[0m,\n", + " \u001b[0m'tap'\u001b[0m,\n", + " \u001b[0m'vlan'\u001b[0m]\u001b[0m,\n", + " \u001b[0m'model'\u001b[0m: \u001b[0m'FIREFLY-PERIMETER'\u001b[0m,\n", + " \u001b[0m'os_version'\u001b[0m: \u001b[0m'12.1X47-D20.7'\u001b[0m,\n", + " \u001b[0m'serial_number'\u001b[0m: \u001b[0m'5efd44465d10'\u001b[0m,\n", + " \u001b[0m'uptime'\u001b[0m: \u001b[0m76648\u001b[0m,\n", + " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Juniper'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'.local.'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'dsc'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:AA:8C:76'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:1A:7F:BF'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:70:E5:81'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'gr-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'gre'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ip-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ipip'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'irb'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'4C:96:14:8C:76:B0'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lo0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lsi'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lsq-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76598.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'02:96:14:8C:76:B3'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'mt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'mtun'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pimd'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pime'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pp0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ppd0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ppe0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sp-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76598.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'st0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'tap'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'vlan'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76607.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'00:00:00:00:00:00'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'leaf00.cmh.cmh.acme.com'\u001b[0m,\n", + " \u001b[0m'hostname'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", + " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", + " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", + " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'uptime'\u001b[0m: \u001b[0m76556\u001b[0m,\n", + " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine00.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516010957.639385\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine01.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939788.3633773\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939804.0736248\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'leaf01.cmh.cmh.acme.com'\u001b[0m,\n", + " \u001b[0m'hostname'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m'ge-0/0/0'\u001b[0m,\n", + " \u001b[0m'gr-0/0/0'\u001b[0m,\n", + " \u001b[0m'ip-0/0/0'\u001b[0m,\n", + " \u001b[0m'lsq-0/0/0'\u001b[0m,\n", + " \u001b[0m'lt-0/0/0'\u001b[0m,\n", + " \u001b[0m'mt-0/0/0'\u001b[0m,\n", + " \u001b[0m'sp-0/0/0'\u001b[0m,\n", + " \u001b[0m'ge-0/0/1'\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m,\n", + " \u001b[0m'.local.'\u001b[0m,\n", + " \u001b[0m'dsc'\u001b[0m,\n", + " \u001b[0m'gre'\u001b[0m,\n", + " \u001b[0m'ipip'\u001b[0m,\n", + " \u001b[0m'irb'\u001b[0m,\n", + " \u001b[0m'lo0'\u001b[0m,\n", + " \u001b[0m'lsi'\u001b[0m,\n", + " \u001b[0m'mtun'\u001b[0m,\n", + " \u001b[0m'pimd'\u001b[0m,\n", + " \u001b[0m'pime'\u001b[0m,\n", + " \u001b[0m'pp0'\u001b[0m,\n", + " \u001b[0m'ppd0'\u001b[0m,\n", + " \u001b[0m'ppe0'\u001b[0m,\n", + " \u001b[0m'st0'\u001b[0m,\n", + " \u001b[0m'tap'\u001b[0m,\n", + " \u001b[0m'vlan'\u001b[0m]\u001b[0m,\n", + " \u001b[0m'model'\u001b[0m: \u001b[0m'FIREFLY-PERIMETER'\u001b[0m,\n", + " \u001b[0m'os_version'\u001b[0m: \u001b[0m'12.1X47-D20.7'\u001b[0m,\n", + " \u001b[0m'serial_number'\u001b[0m: \u001b[0m'9d842799f666'\u001b[0m,\n", + " \u001b[0m'uptime'\u001b[0m: \u001b[0m76460\u001b[0m,\n", + " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Juniper'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'.local.'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'dsc'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:AA:8C:76'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine00.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:A0:42:60'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine01.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:00:6D:5A'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", + " \u001b[0m'gr-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'gre'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ip-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ipip'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'irb'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'4C:96:14:8C:76:B0'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lo0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lsi'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lsq-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76408.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'lt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'02:96:14:8C:76:B3'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[0m'mt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'mtun'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pimd'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pime'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'pp0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ppd0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ppe0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sp-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76408.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", + " \u001b[0m'st0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'tap'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", + " \u001b[0m'vlan'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76417.0\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'00:00:00:00:00:00'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run get_facts.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/1_simple_runbooks/get_facts.py b/examples/1_simple_runbooks/get_facts.py index 89bd5a05..5b308998 100755 --- a/examples/1_simple_runbooks/get_facts.py +++ b/examples/1_simple_runbooks/get_facts.py @@ -1,30 +1,32 @@ #!/usr/bin/env python """ -This is a very simple runbook to get facts and print them on the screen. +Very simple runbook to get facts and print them on the screen. """ from brigade.easy import easy_brigade from brigade.plugins.functions.text import print_title -from brigade.plugins.tasks.networking import napalm_get -from brigade.plugins.tasks.text import print_result +from brigade.plugins.tasks import networking, text -brigade = easy_brigade( - hosts="../hosts.yaml", groups="../groups.yaml", - dry_run=True, +brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, raise_on_error=False, ) print_title("Getting interfaces and facts") # select which devices we want to work with -filtered = brigade.filter(type="network_device", site="cmh") +filtered = brg.filter(type="network_device", site="cmh") -# we are going to gather "interfaces" and "facts" information with napalm -results = filtered.run(napalm_get, +# we are going to gather "interfaces" and "facts" +# information with napalm +results = filtered.run(networking.napalm_get, getters=["interfaces", "facts"]) # Let's print the result on screen -filtered.run(print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) +filtered.run(text.print_result, + num_workers=1, # task should be done synchronously + data=results, + ) diff --git a/examples/1_simple_runbooks/rollback.ipynb b/examples/1_simple_runbooks/rollback.ipynb new file mode 100644 index 00000000..e99f693f --- /dev/null +++ b/examples/1_simple_runbooks/rollback.ipynb @@ -0,0 +1,438 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Rollback\n", + "\n", + "This runbook plays well with the ``backup.py`` one. You can basically backup the configuration, and then roll it back with this runbook if things don't go as expected.\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Runbook to rollback configuration from a saved configuration\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import networking, text\n",
+       " 7 \n",
+       " 8 \n",
+       " 9 def rollback(task):\n",
+       "10     """\n",
+       "11     This function loads the backup from ./backups/$hostname and\n",
+       "12     deploys it.\n",
+       "13     """\n",
+       "14     task.run(networking.napalm_configure,\n",
+       "15              name="Loading Configuration on the device",\n",
+       "16              replace=True,\n",
+       "17              filename="backups/{host}")\n",
+       "18 \n",
+       "19 \n",
+       "20 brg = easy_brigade(\n",
+       "21         host_file="../inventory/hosts.yaml",\n",
+       "22         group_file="../inventory/groups.yaml",\n",
+       "23         dry_run=False,\n",
+       "24         raise_on_error=True,\n",
+       "25 )\n",
+       "26 \n",
+       "27 \n",
+       "28 # select which devices we want to work with\n",
+       "29 filtered = brg.filter(type="network_device", site="cmh")\n",
+       "30 \n",
+       "31 results = filtered.run(task=rollback)\n",
+       "32 \n",
+       "33 filtered.run(text.print_result,\n",
+       "34              num_workers=1,  # task should be done synchronously\n",
+       "35              data=results,\n",
+       "36              task_id=-1,  # we only want to print the last task\n",
+       "37              )\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file rollback.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "So let's rollback to the backup configuration we took before configuring the network early on:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,8 +8,7 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "-hostname spine00.cmh\n", + "-ip domain-name cmh.acme.com\n", + "+hostname blah\n", + " !\n", + " spanning-tree mode mstp\n", + " !\n", + "@@ -21,28 +20,13 @@\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + " !\n", + " interface Ethernet1\n", + "- description link to leaf00.cmh\n", + "- no switchport\n", + "- ip address 10.0.0.0/31\n", + " !\n", + " interface Ethernet2\n", + "- description link to leaf01.cmh\n", + "- no switchport\n", + "- ip address 10.0.0.2/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-ip routing\n", + "-!\n", + "-router bgp 65000\n", + "- neighbor 10.0.0.1 remote-as 65100\n", + "- neighbor 10.0.0.1 maximum-routes 12000 \n", + "- neighbor 10.0.0.3 remote-as 65101\n", + "- neighbor 10.0.0.3 maximum-routes 12000 \n", + "- address-family ipv4\n", + "- neighbor 10.0.0.1 activate\n", + "- neighbor 10.0.0.3 activate\n", + "+no ip routing\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name spine01.cmh;\n", + "+ host-name vsrx;\n", + "- domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "- ge-0/0/1 {\n", + "- description \"link to leaf00.cmh\";\n", + "- unit 0 {\n", + "- family inet {\n", + "- address 10.0.1.0/31;\n", + "- }\n", + "- }\n", + "- }\n", + "- ge-0/0/2 {\n", + "- description \"link to leaf01.cmh\";\n", + "- unit 0 {\n", + "- family inet {\n", + "- address 10.0.1.2/31;\n", + "- }\n", + "- }\n", + "- }\n", + "[edit]\n", + "- routing-options {\n", + "- autonomous-system 65000;\n", + "- }\n", + "- protocols {\n", + "- bgp {\n", + "- import PERMIT_ALL;\n", + "- export PERMIT_ALL;\n", + "- group peers {\n", + "- neighbor 10.0.1.1 {\n", + "- peer-as 65100;\n", + "- }\n", + "- neighbor 10.0.1.3 {\n", + "- peer-as 65101;\n", + "- }\n", + "- }\n", + "- }\n", + "- }\n", + "- policy-options {\n", + "- policy-statement PERMIT_ALL {\n", + "- from protocol bgp;\n", + "- then accept;\n", + "- }\n", + "- }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,9 +8,6 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "-hostname leaf00.cmh\n", + "-ip domain-name cmh.acme.com\n", + "-!\n", + " spanning-tree mode mstp\n", + " !\n", + " aaa authorization exec default local\n", + "@@ -20,36 +17,14 @@\n", + " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + " !\n", + "-vlan 100\n", + "- name frontend\n", + "-!\n", + "-vlan 200\n", + "- name backend\n", + "-!\n", + " interface Ethernet1\n", + "- description link to spine00.cmh\n", + "- shutdown\n", + "- no switchport\n", + "- ip address 10.0.0.1/31\n", + " !\n", + " interface Ethernet2\n", + "- description link to spine01.cmh\n", + "- no switchport\n", + "- ip address 10.0.1.1/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-ip routing\n", + "-!\n", + "-router bgp 65100\n", + "- neighbor 10.0.0.0 remote-as 65000\n", + "- neighbor 10.0.0.0 maximum-routes 12000 \n", + "- neighbor 10.0.1.0 remote-as 65000\n", + "- neighbor 10.0.1.0 maximum-routes 12000 \n", + "- address-family ipv4\n", + "- neighbor 10.0.0.0 activate\n", + "- neighbor 10.0.1.0 activate\n", + "+no ip routing\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name leaf01.cmh;\n", + "+ host-name vsrx;\n", + "- domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "- ge-0/0/1 {\n", + "- description \"link to spine00.cmh\";\n", + "- unit 0 {\n", + "- family inet {\n", + "- address 10.0.0.3/31;\n", + "- }\n", + "- }\n", + "- }\n", + "- ge-0/0/2 {\n", + "- description \"link to spine01.cmh\";\n", + "- unit 0 {\n", + "- family inet {\n", + "- address 10.0.1.3/31;\n", + "- }\n", + "- }\n", + "- }\n", + "[edit]\n", + "- routing-options {\n", + "- autonomous-system 65101;\n", + "- }\n", + "- protocols {\n", + "- bgp {\n", + "- import PERMIT_ALL;\n", + "- export PERMIT_ALL;\n", + "- group peers {\n", + "- neighbor 10.0.0.2 {\n", + "- peer-as 65000;\n", + "- }\n", + "- neighbor 10.0.1.2 {\n", + "- peer-as 65000;\n", + "- }\n", + "- }\n", + "- }\n", + "- }\n", + "- policy-options {\n", + "- policy-statement PERMIT_ALL {\n", + "- from protocol bgp;\n", + "- then accept;\n", + "- }\n", + "- }\n", + "- vlans {\n", + "- backend {\n", + "- vlan-id 200;\n", + "- }\n", + "- frontend {\n", + "- vlan-id 100;\n", + "- }\n", + "- }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with other tasks, changes are detected and only when needed are applied:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/1_simple_runbooks/rollback.py b/examples/1_simple_runbooks/rollback.py new file mode 100755 index 00000000..655f07ac --- /dev/null +++ b/examples/1_simple_runbooks/rollback.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +""" +Runbook to rollback configuration from a saved configuration +""" +from brigade.easy import easy_brigade +from brigade.plugins.tasks import networking, text + +import click + + +def rollback(task): + """ + This function loads the backup from ./backups/$hostname and + deploys it. + """ + task.run(networking.napalm_configure, + name="Loading Configuration on the device", + replace=True, + filename="backups/{host}") + + +@click.command() +@click.option('--filter', '-f', multiple=True, + help="k=v pairs to filter the devices") +@click.option('--get', '-g', multiple=True, + help="getters you want to use") +def main(filter, get): + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=True, + ) + + + # select which devices we want to work with + filtered = brg.filter(type="network_device", site="cmh") + + results = filtered.run(task=rollback) + + filtered.run(text.print_result, + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + ) +if __name__ == "__main__": + main() diff --git a/examples/1_simple_runbooks/validate.ipynb b/examples/1_simple_runbooks/validate.ipynb new file mode 100644 index 00000000..8af4499e --- /dev/null +++ b/examples/1_simple_runbooks/validate.ipynb @@ -0,0 +1,689 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Validate\n", + "\n", + "This playbook uses [napalm validation](http://napalm.readthedocs.io/en/latest/validate/index.html) functionality to verify correctness of the network.\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Runbook that verifies that BGP sessions are configured and up.\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import data, networking, text\n",
+       " 7 \n",
+       " 8 \n",
+       " 9 def validate(task):\n",
+       "10     task.host["config"] = ""\n",
+       "11 \n",
+       "12     r = task.run(name="read data",\n",
+       "13                  task=data.load_yaml,\n",
+       "14                  file="../extra_data/{host}/l3.yaml")\n",
+       "15 \n",
+       "16     validation_rules = [{\n",
+       "17         'get_bgp_neighbors': {\n",
+       "18             'global': {\n",
+       "19                 'peers': {\n",
+       "20                     '_mode': 'strict',\n",
+       "21                 }\n",
+       "22             }\n",
+       "23         }\n",
+       "24     }]\n",
+       "25     peers = validation_rules[0]['get_bgp_neighbors']['global']['peers']\n",
+       "26     for session in r.result['sessions']:\n",
+       "27         peers[session['ipv4']] = {'is_up': True}\n",
+       "28 \n",
+       "29     task.run(name="validating data",\n",
+       "30              task=networking.napalm_validate,\n",
+       "31              validation_source=validation_rules)\n",
+       "32 \n",
+       "33 \n",
+       "34 def print_compliance(task, results):\n",
+       "35     """\n",
+       "36     We use this task so we can access directly the result\n",
+       "37     for each specific host and see if the task complies or not\n",
+       "38     and pass it to print_result.\n",
+       "39     """\n",
+       "40     task.run(text.print_result,\n",
+       "41              name="print result",\n",
+       "42              data=results[task.host.name],\n",
+       "43              failed=not results[task.host.name][2].result['complies'],\n",
+       "44              )\n",
+       "45 \n",
+       "46 \n",
+       "47 brg = easy_brigade(\n",
+       "48         host_file="../inventory/hosts.yaml",\n",
+       "49         group_file="../inventory/groups.yaml",\n",
+       "50         dry_run=False,\n",
+       "51         raise_on_error=True,\n",
+       "52 )\n",
+       "53 \n",
+       "54 \n",
+       "55 filtered = brg.filter(type="network_device", site="cmh")\n",
+       "56 \n",
+       "57 results = filtered.run(task=validate)\n",
+       "58 \n",
+       "59 filtered.run(print_compliance,\n",
+       "60              results=results,\n",
+       "61              num_workers=1)\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file validate.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Let's start by running the script on an unconfigured network:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run validate.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For each host we get the data we are using for validation and the result. What the report is saying is that we don't even have the BGP instance 'global' (default instance) configured so let's do it:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,7 +8,8 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "-hostname blah\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + " !\n", + " spanning-tree mode mstp\n", + " !\n", + "@@ -20,13 +21,28 @@\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + " !\n", + " interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65000;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,6 +8,9 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + " spanning-tree mode mstp\n", + " !\n", + " aaa authorization exec default local\n", + "@@ -17,14 +20,36 @@\n", + " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + " !\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + " interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to spine00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.0.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to spine01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65101;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.0.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ neighbor 10.0.1.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\n", + "+ vlans {\n", + "+ backend {\n", + "+ vlan-id 200;\n", + "+ }\n", + "+ frontend {\n", + "+ vlan-id 100;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the network is configured let's validate the deployment again:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", + " \u001b[0m'10.0.0.3'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", + " \u001b[0m'10.0.1.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run validate.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What the report is basically telling us is that ``spina01`` and ``leaf01`` are pssing our tests, however, ``spine00`` and ``leaf00`` as one of their BGP sessions that should be ``up`` is actually ``down``." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/1_simple_runbooks/validate.py b/examples/1_simple_runbooks/validate.py index 6ef85baf..4453babd 100755 --- a/examples/1_simple_runbooks/validate.py +++ b/examples/1_simple_runbooks/validate.py @@ -1,9 +1,8 @@ #!/usr/bin/env python """ -In this example we write a CLI tool with brigade and click to deploy configuration. +Runbook that verifies that BGP sessions are configured and up. """ -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade from brigade.plugins.tasks import data, networking, text @@ -27,25 +26,33 @@ def validate(task): for session in r.result['sessions']: peers[session['ipv4']] = {'is_up': True} - return task.run(name="validating data", - task=networking.napalm_validate, - validation_source=validation_rules) + task.run(name="validating data", + task=networking.napalm_validate, + validation_source=validation_rules) def print_compliance(task, results): - task.run(name="print result", - task=text.print_result, + """ + We use this task so we can access directly the result + for each specific host and see if the task complies or not + and pass it to print_result. + """ + task.run(text.print_result, + name="print result", data=results[task.host.name], - failed=not results[task.host.name].result['complies'], + failed=not results[task.host.name][2].result['complies'], ) -brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=False, +brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=True, ) -filtered = brigade.filter(type="network_device", site="cmh") + +filtered = brg.filter(type="network_device", site="cmh") results = filtered.run(task=validate) diff --git a/examples/2_simple_tooling/backup.ipynb b/examples/2_simple_tooling/backup.ipynb new file mode 100644 index 00000000..7df81f0d --- /dev/null +++ b/examples/2_simple_tooling/backup.ipynb @@ -0,0 +1,1062 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Backup\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Tool that downloads the configuration from the devices and\n",
+       " 4 stores them on disk.\n",
+       " 5 """\n",
+       " 6 from brigade.easy import easy_brigade\n",
+       " 7 from brigade.plugins.tasks import files, networking, text\n",
+       " 8 \n",
+       " 9 import click\n",
+       "10 \n",
+       "11 \n",
+       "12 def backup(task, path):\n",
+       "13     """\n",
+       "14     This function groups two tasks:\n",
+       "15         1. Download configuration from the device\n",
+       "16         2. Store to disk\n",
+       "17     """\n",
+       "18     result = task.run(networking.napalm_get,\n",
+       "19                       name="Gathering configuration from the device",\n",
+       "20                       getters="config")\n",
+       "21 \n",
+       "22     task.run(files.write,\n",
+       "23              name="Saving Configuration to disk",\n",
+       "24              content=result.result["config"]["running"],\n",
+       "25              filename="{}/{}".format(path, task.host))\n",
+       "26 \n",
+       "27 \n",
+       "28 @click.command()\n",
+       "29 @click.option('--filter', '-f', multiple=True,\n",
+       "30               help="filters to apply. For instance site=cmh")\n",
+       "31 @click.option('--path', '-p', default=".",\n",
+       "32               help="Where to save the backup files")\n",
+       "33 def main(filter, path):\n",
+       "34     """\n",
+       "35     Backups running configuration of devices into a file\n",
+       "36     """\n",
+       "37     brg = easy_brigade(\n",
+       "38             host_file="../inventory/hosts.yaml",\n",
+       "39             group_file="../inventory/groups.yaml",\n",
+       "40             dry_run=False,\n",
+       "41             raise_on_error=False,\n",
+       "42     )\n",
+       "43 \n",
+       "44     # filter is going to be a list of key=value so we clean that first\n",
+       "45     filter_dict = {"type": "network_device"}\n",
+       "46     for f in filter:\n",
+       "47         k, v = f.split("=")\n",
+       "48         filter_dict[k] = v\n",
+       "49 \n",
+       "50     # let's filter the devices\n",
+       "51     filtered = brg.filter(**filter_dict)\n",
+       "52 \n",
+       "53     # Run the ``backup`` function that groups the tasks to\n",
+       "54     # download/store devices' configuration\n",
+       "55     results = filtered.run(backup,\n",
+       "56                            name="Backing up configurations",\n",
+       "57                            path=path)\n",
+       "58 \n",
+       "59     # Let's print the result on screen\n",
+       "60     filtered.run(text.print_result,\n",
+       "61                  num_workers=1,  # task should be done synchronously\n",
+       "62                  data=results,\n",
+       "63                  task_id=-1,  # we only want to print the last task\n",
+       "64                  skipped=True,\n",
+       "65                  )\n",
+       "66 \n",
+       "67 \n",
+       "68 if __name__ == "__main__":\n",
+       "69     main()\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file backup.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Let's start with the help so we can see what we can do." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Usage: backup.py [OPTIONS]\n", + "\n", + " Backups running configuration of devices into a file\n", + "\n", + "Options:\n", + " -f, --filter TEXT filters to apply. For instance site=cmh\n", + " -p, --path TEXT Where to save the backup files\n", + " --help Show this message and exit.\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run backup.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With those options it should be easy to backup devices at different sites in different paths:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mkdir: backups/cmh: File exists\n", + "\u001b[0m\u001b[0m\u001b[0m\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/cmh//spine00.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,52 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: spine00.cmh (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + "+!\n", + "+interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + "+!\n", + "+interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/cmh//spine01.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,110 @@\n", + "\n", + "+\n", + "+## Last commit: 2018-01-15 12:02:22 UTC by vagrant\n", + "+version 12.1X47-D20.7;\n", + "+system {\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "+ root-authentication {\n", + "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", + "+ }\n", + "+ login {\n", + "+ user vagrant {\n", + "+ uid 2000;\n", + "+ class super-user;\n", + "+ authentication {\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ services {\n", + "+ ssh {\n", + "+ root-login allow;\n", + "+ }\n", + "+ netconf {\n", + "+ ssh;\n", + "+ }\n", + "+ web-management {\n", + "+ http {\n", + "+ interface ge-0/0/0.0;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ syslog {\n", + "+ user * {\n", + "+ any emergency;\n", + "+ }\n", + "+ file messages {\n", + "+ any any;\n", + "+ authorization info;\n", + "+ }\n", + "+ file interactive-commands {\n", + "+ interactive-commands any;\n", + "+ }\n", + "+ }\n", + "+ license {\n", + "+ autoupdate {\n", + "+ url https://ae1.juniper.net/junos/key_retrieval;\n", + "+ }\n", + "+ }\n", + "+}\n", + "+interfaces {\n", + "+ ge-0/0/0 {\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ dhcp;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+routing-options {\n", + "+ autonomous-system 65000;\n", + "+}\n", + "+protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+}\n", + "+security {\n", + "+ forwarding-options {\n", + "+ family {\n", + "+ inet6 {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ mpls {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/cmh//leaf00.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,59 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: leaf00.cmh (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + "+!\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + "+interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + "+!\n", + "+interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/cmh//leaf01.cmh\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,118 @@\n", + "\n", + "+\n", + "+## Last commit: 2018-01-15 12:02:23 UTC by vagrant\n", + "+version 12.1X47-D20.7;\n", + "+system {\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "+ root-authentication {\n", + "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\";\n", + "+ }\n", + "+ login {\n", + "+ user vagrant {\n", + "+ uid 2000;\n", + "+ class super-user;\n", + "+ authentication {\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ services {\n", + "+ ssh {\n", + "+ root-login allow;\n", + "+ }\n", + "+ netconf {\n", + "+ ssh;\n", + "+ }\n", + "+ web-management {\n", + "+ http {\n", + "+ interface ge-0/0/0.0;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ syslog {\n", + "+ user * {\n", + "+ any emergency;\n", + "+ }\n", + "+ file messages {\n", + "+ any any;\n", + "+ authorization info;\n", + "+ }\n", + "+ file interactive-commands {\n", + "+ interactive-commands any;\n", + "+ }\n", + "+ }\n", + "+ license {\n", + "+ autoupdate {\n", + "+ url https://ae1.juniper.net/junos/key_retrieval;\n", + "+ }\n", + "+ }\n", + "+}\n", + "+interfaces {\n", + "+ ge-0/0/0 {\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ dhcp;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/1 {\n", + "+ description \"link to spine00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.0.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to spine01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+routing-options {\n", + "+ autonomous-system 65101;\n", + "+}\n", + "+protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.0.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ neighbor 10.0.1.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+}\n", + "+security {\n", + "+ forwarding-options {\n", + "+ family {\n", + "+ inet6 {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ mpls {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+vlans {\n", + "+ backend {\n", + "+ vlan-id 200;\n", + "+ }\n", + "+ frontend {\n", + "+ vlan-id 100;\n", + "+ }\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%mkdir backups/cmh\n", + "%rm backups/cmh/*\n", + "%run backup.py --filter site=cmh --path backups/cmh/" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mkdir: backups/bma: File exists\n", + "\u001b[0m\u001b[0m\u001b[0m\u001b[0m\u001b[1m\u001b[33m* spine00.bma ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/bma//spine00.bma\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,52 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: spine00.cmh (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + "+!\n", + "+interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + "+!\n", + "+interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.bma ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/bma//spine01.bma\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,110 @@\n", + "\n", + "+\n", + "+## Last commit: 2018-01-15 12:02:22 UTC by vagrant\n", + "+version 12.1X47-D20.7;\n", + "+system {\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "+ root-authentication {\n", + "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", + "+ }\n", + "+ login {\n", + "+ user vagrant {\n", + "+ uid 2000;\n", + "+ class super-user;\n", + "+ authentication {\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ services {\n", + "+ ssh {\n", + "+ root-login allow;\n", + "+ }\n", + "+ netconf {\n", + "+ ssh;\n", + "+ }\n", + "+ web-management {\n", + "+ http {\n", + "+ interface ge-0/0/0.0;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ syslog {\n", + "+ user * {\n", + "+ any emergency;\n", + "+ }\n", + "+ file messages {\n", + "+ any any;\n", + "+ authorization info;\n", + "+ }\n", + "+ file interactive-commands {\n", + "+ interactive-commands any;\n", + "+ }\n", + "+ }\n", + "+ license {\n", + "+ autoupdate {\n", + "+ url https://ae1.juniper.net/junos/key_retrieval;\n", + "+ }\n", + "+ }\n", + "+}\n", + "+interfaces {\n", + "+ ge-0/0/0 {\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ dhcp;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+routing-options {\n", + "+ autonomous-system 65000;\n", + "+}\n", + "+protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\n", + "+policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+}\n", + "+security {\n", + "+ forwarding-options {\n", + "+ family {\n", + "+ inet6 {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ mpls {\n", + "+ mode packet-based;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.bma ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", + "\u001b[0m--- backups/bma//leaf00.bma\n", + "\n", + "+++ new\n", + "\n", + "@@ -0,0 +1,59 @@\n", + "\n", + "+! Command: show running-config\n", + "+! device: leaf00.cmh (vEOS, EOS-4.17.5M)\n", + "+!\n", + "+! boot system flash:/vEOS-lab.swi\n", + "+!\n", + "+event-handler dhclient\n", + "+ trigger on-boot\n", + "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", + "+!\n", + "+transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + "+spanning-tree mode mstp\n", + "+!\n", + "+aaa authorization exec default local\n", + "+!\n", + "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", + "+!\n", + "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + "+!\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + "+interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + "+!\n", + "+interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + "+!\n", + "+interface Management1\n", + "+ ip address 10.0.2.15/24\n", + "+!\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + "+!\n", + "+management api http-commands\n", + "+ no shutdown\n", + "+!\n", + "+!\n", + "+end\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* leaf01.bma ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Backing up configurations ** changed : False -----------------------------\u001b[0m\n", + "\u001b[0mTraceback (most recent call last):\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1250, in open\n", + " device_params={'name': 'junos', 'local': False})\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 154, in connect\n", + " return connect_ssh(*args, **kwds)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 119, in connect_ssh\n", + " session.connect(*args, **kwds)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 412, in connect\n", + " self._auth(username, password, key_filenames, allow_agent, look_for_keys)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 508, in _auth\n", + " raise AuthenticationError(repr(saved_exception))\n", + "ncclient.transport.errors.AuthenticationError: AuthenticationException('Authentication failed.',)\n", + "\n", + "During handling of the above exception, another exception occurred:\n", + "\n", + "Traceback (most recent call last):\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", + " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/examples/2_simple_tooling/backup.py\", line 20, in backup\n", + " getters=\"config\")\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 67, in run\n", + " r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/networking/napalm_get.py\", line 16, in napalm_get\n", + " device = task.host.get_connection(\"napalm\")\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/inventory.py\", line 212, in get_connection\n", + " raise r[self.name].exception\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", + " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/connections/napalm_connection.py\", line 31, in napalm_connection\n", + " host.connections[\"napalm\"].open()\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/napalm/junos/junos.py\", line 106, in open\n", + " self.device.open()\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1254, in open\n", + " raise EzErrors.ConnectAuthError(self)\n", + "jnpr.junos.exception.ConnectAuthError: ConnectAuthError(127.0.0.1)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%mkdir backups/bma\n", + "%rm backups/bma/*\n", + "%run backup.py --filter site=bma --path backups/bma/" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[31m* leaf01.bma ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Backing up configurations ** changed : False -----------------------------\u001b[0m\n", + "\u001b[0mTraceback (most recent call last):\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1250, in open\n", + " device_params={'name': 'junos', 'local': False})\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 154, in connect\n", + " return connect_ssh(*args, **kwds)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 119, in connect_ssh\n", + " session.connect(*args, **kwds)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 412, in connect\n", + " self._auth(username, password, key_filenames, allow_agent, look_for_keys)\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 508, in _auth\n", + " raise AuthenticationError(repr(saved_exception))\n", + "ncclient.transport.errors.AuthenticationError: AuthenticationException('Authentication failed.',)\n", + "\n", + "During handling of the above exception, another exception occurred:\n", + "\n", + "Traceback (most recent call last):\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", + " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/examples/2_simple_tooling/backup.py\", line 20, in backup\n", + " getters=\"config\")\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 67, in run\n", + " r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/networking/napalm_get.py\", line 16, in napalm_get\n", + " device = task.host.get_connection(\"napalm\")\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/inventory.py\", line 212, in get_connection\n", + " raise r[self.name].exception\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", + " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", + " r = self.task(self, **self.params) or Result(host)\n", + " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/connections/napalm_connection.py\", line 31, in napalm_connection\n", + " host.connections[\"napalm\"].open()\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/napalm/junos/junos.py\", line 106, in open\n", + " self.device.open()\n", + " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1254, in open\n", + " raise EzErrors.ConnectAuthError(self)\n", + "jnpr.junos.exception.ConnectAuthError: ConnectAuthError(127.0.0.1)\n", + "\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run backup.py --filter name=leaf01.bma --path backups/bma/" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note that brigade detected and reported that we failed to authenticate to one of the devices.\n", + "\n", + "Now we can check we have the backups in the right place:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "backups/bma:\n", + "leaf00.bma spine00.bma spine01.bma\n", + "\n", + "backups/cmh:\n", + "leaf00.cmh leaf01.cmh spine00.cmh spine01.cmh\n", + "\u001b[0m\u001b[0m" + ] + } + ], + "source": [ + "% ls backups/*" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/2_simple_tooling/backup.py b/examples/2_simple_tooling/backup.py index bf68c168..ba3d79b3 100755 --- a/examples/2_simple_tooling/backup.py +++ b/examples/2_simple_tooling/backup.py @@ -1,40 +1,44 @@ #!/usr/bin/env python """ -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. - -The main difference with get_facts_simple.py is that instead of calling a plugin directly -we wrap it in a function. It is not very useful or necessary here but illustrates how -tasks can be grouped. +Tool that downloads the configuration from the devices and +stores them on disk. """ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade from brigade.plugins.tasks import files, networking, text import click def backup(task, path): + """ + This function groups two tasks: + 1. Download configuration from the device + 2. Store to disk + """ result = task.run(networking.napalm_get, + name="Gathering configuration from the device", getters="config") - return task.run(files.write, - content=result.result["config"]["running"], - filename="{}/{}".format(path, task.host)) + task.run(files.write, + name="Saving Configuration to disk", + content=result.result["config"]["running"], + filename="{}/{}".format(path, task.host)) @click.command() -@click.option('--filter', '-f', multiple=True) -@click.option('--path', '-p', default=".") +@click.option('--filter', '-f', multiple=True, + help="filters to apply. For instance site=cmh") +@click.option('--path', '-p', default=".", + help="Where to save the backup files") def main(filter, path): """ Backups running configuration of devices into a file """ - brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=False, - config=Config(raise_on_error=False), + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=False, ) # filter is going to be a list of key=value so we clean that first @@ -43,13 +47,22 @@ def main(filter, path): k, v = f.split("=") filter_dict[k] = v - filtered = brigade.filter(**filter_dict) # let's filter the devices - results = filtered.run(backup, num_workers=20, path=path) + # let's filter the devices + filtered = brg.filter(**filter_dict) + + # Run the ``backup`` function that groups the tasks to + # download/store devices' configuration + results = filtered.run(backup, + name="Backing up configurations", + path=path) # Let's print the result on screen filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + skipped=True, + ) if __name__ == "__main__": diff --git a/examples/2_simple_tooling/configure.ipynb b/examples/2_simple_tooling/configure.ipynb new file mode 100644 index 00000000..fd28dd6c --- /dev/null +++ b/examples/2_simple_tooling/configure.ipynb @@ -0,0 +1,747 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Configure\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Tool to configure datacenter\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import data, networking, text\n",
+       " 7 \n",
+       " 8 import click\n",
+       " 9 \n",
+       "10 \n",
+       "11 def configure(task):\n",
+       "12     """\n",
+       "13     This function groups all the tasks needed to configure the\n",
+       "14     network:\n",
+       "15 \n",
+       "16         1. Loading extra data\n",
+       "17         2. Templates to build configuration\n",
+       "18         3. Deploy configuration on the device\n",
+       "19     """\n",
+       "20     r = task.run(text.template_file,\n",
+       "21                  name="Base Configuration",\n",
+       "22                  template="base.j2",\n",
+       "23                  path="../templates/{brigade_nos}")\n",
+       "24     # r.result holds the result of rendering the template\n",
+       "25     # we store in the host itself so we can keep updating\n",
+       "26     # it as we render other templates\n",
+       "27     task.host["config"] = r.result\n",
+       "28 \n",
+       "29     r = task.run(data.load_yaml,\n",
+       "30                  name="Loading extra data",\n",
+       "31                  file="../extra_data/{host}/l3.yaml")\n",
+       "32     # r.result holds the data contained in the yaml files\n",
+       "33     # we load the data inside the host itself for further use\n",
+       "34     task.host["l3"] = r.result\n",
+       "35 \n",
+       "36     r = task.run(text.template_file,\n",
+       "37                  name="Interfaces Configuration",\n",
+       "38                  template="interfaces.j2",\n",
+       "39                  path="../templates/{brigade_nos}")\n",
+       "40     # we update our hosts' config\n",
+       "41     task.host["config"] += r.result\n",
+       "42 \n",
+       "43     r = task.run(text.template_file,\n",
+       "44                  name="Routing Configuration",\n",
+       "45                  template="routing.j2",\n",
+       "46                  path="../templates/{brigade_nos}")\n",
+       "47     # we update our hosts' config\n",
+       "48     task.host["config"] += r.result\n",
+       "49 \n",
+       "50     r = task.run(text.template_file,\n",
+       "51                  name="Role-specific Configuration",\n",
+       "52                  template="{role}.j2",\n",
+       "53                  path="../templates/{brigade_nos}")\n",
+       "54     # we update our hosts' config\n",
+       "55     task.host["config"] += r.result\n",
+       "56 \n",
+       "57     task.run(networking.napalm_configure,\n",
+       "58              name="Loading Configuration on the device",\n",
+       "59              replace=False,\n",
+       "60              configuration=task.host["config"])\n",
+       "61 \n",
+       "62 \n",
+       "63 @click.command()\n",
+       "64 @click.option('--filter', '-f', multiple=True,\n",
+       "65               help="k=v pairs to filter the devices")\n",
+       "66 @click.option('--commit/--no-commit', '-c', default=False,\n",
+       "67               help="whether you want to commit the changes or not")\n",
+       "68 def main(filter, commit):\n",
+       "69     brg = easy_brigade(\n",
+       "70             host_file="../inventory/hosts.yaml",\n",
+       "71             group_file="../inventory/groups.yaml",\n",
+       "72             dry_run=not commit,\n",
+       "73             raise_on_error=False,\n",
+       "74     )\n",
+       "75 \n",
+       "76     # filter is going to be a list of key=value so we clean that first\n",
+       "77     filter_dict = {"type": "network_device"}\n",
+       "78     for f in filter:\n",
+       "79         k, v = f.split("=")\n",
+       "80         filter_dict[k] = v\n",
+       "81 \n",
+       "82     # let's filter the devices\n",
+       "83     filtered = brg.filter(**filter_dict)\n",
+       "84 \n",
+       "85     results = filtered.run(task=configure)\n",
+       "86 \n",
+       "87     filtered.run(text.print_result,\n",
+       "88                  num_workers=1,  # task should be done synchronously\n",
+       "89                  data=results,\n",
+       "90                  task_id=-1,  # we only want to print the last task\n",
+       "91                  skipped=True,\n",
+       "92                  )\n",
+       "93 \n",
+       "94 \n",
+       "95 if __name__ == "__main__":\n",
+       "96     main()\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file configure.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "Let's start with the help so we can see what we can do." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Usage: configure.py [OPTIONS]\n", + "\n", + "Options:\n", + " -f, --filter TEXT k=v pairs to filter the devices\n", + " -c, --commit / --no-commit whether you want to commit the changes or not\n", + " --help Show this message and exit.\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With those options it should be easy to check which changes are to be applied before even applying them." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + " !\n", + " spanning-tree mode mstp\n", + " !\n", + "@@ -18,13 +21,28 @@\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", + " !\n", + " interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65000;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,6 +8,9 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + " spanning-tree mode mstp\n", + " !\n", + " aaa authorization exec default local\n", + "@@ -17,14 +20,36 @@\n", + " username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", + " !\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + " interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to spine00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.0.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to spine01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65101;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.0.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ neighbor 10.0.1.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\n", + "+ vlans {\n", + "+ backend {\n", + "+ vlan-id 200;\n", + "+ }\n", + "+ frontend {\n", + "+ vlan-id 100;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py --filter site=cmh --no-commit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can review the changes and commit them if we are happy:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.com\n", + " !\n", + " spanning-tree mode mstp\n", + " !\n", + "@@ -18,13 +21,28 @@\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", + " !\n", + " interface Ethernet1\n", + "+ description link to leaf00.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.0/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to leaf01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.0.2/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65000\n", + "+ neighbor 10.0.0.1 remote-as 65100\n", + "+ neighbor 10.0.0.1 maximum-routes 12000 \n", + "+ neighbor 10.0.0.3 remote-as 65101\n", + "+ neighbor 10.0.0.3 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.1 activate\n", + "+ neighbor 10.0.0.3 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to leaf00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.0/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to leaf01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.2/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65000;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.1.1 {\n", + "+ peer-as 65100;\n", + "+ }\n", + "+ neighbor 10.0.1.3 {\n", + "+ peer-as 65101;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -8,6 +8,9 @@\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + " !\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.com\n", + "+!\n", + " spanning-tree mode mstp\n", + " !\n", + " aaa authorization exec default local\n", + "@@ -17,14 +20,36 @@\n", + " username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", + " username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", + " !\n", + "+vlan 100\n", + "+ name frontend\n", + "+!\n", + "+vlan 200\n", + "+ name backend\n", + "+!\n", + " interface Ethernet1\n", + "+ description link to spine00.cmh\n", + "+ shutdown\n", + "+ no switchport\n", + "+ ip address 10.0.0.1/31\n", + " !\n", + " interface Ethernet2\n", + "+ description link to spine01.cmh\n", + "+ no switchport\n", + "+ ip address 10.0.1.1/31\n", + " !\n", + " interface Management1\n", + " ip address 10.0.2.15/24\n", + " !\n", + "-no ip routing\n", + "+ip routing\n", + "+!\n", + "+router bgp 65100\n", + "+ neighbor 10.0.0.0 remote-as 65000\n", + "+ neighbor 10.0.0.0 maximum-routes 12000 \n", + "+ neighbor 10.0.1.0 remote-as 65000\n", + "+ neighbor 10.0.1.0 maximum-routes 12000 \n", + "+ address-family ipv4\n", + "+ neighbor 10.0.0.0 activate\n", + "+ neighbor 10.0.1.0 activate\n", + " !\n", + " management api http-commands\n", + " no shutdown\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.com;\n", + "[edit interfaces]\n", + "+ ge-0/0/1 {\n", + "+ description \"link to spine00.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.0.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ ge-0/0/2 {\n", + "+ description \"link to spine01.cmh\";\n", + "+ unit 0 {\n", + "+ family inet {\n", + "+ address 10.0.1.3/31;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "[edit]\n", + "+ routing-options {\n", + "+ autonomous-system 65101;\n", + "+ }\n", + "+ protocols {\n", + "+ bgp {\n", + "+ import PERMIT_ALL;\n", + "+ export PERMIT_ALL;\n", + "+ group peers {\n", + "+ neighbor 10.0.0.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ neighbor 10.0.1.2 {\n", + "+ peer-as 65000;\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ }\n", + "+ policy-options {\n", + "+ policy-statement PERMIT_ALL {\n", + "+ from protocol bgp;\n", + "+ then accept;\n", + "+ }\n", + "+ }\n", + "+ vlans {\n", + "+ backend {\n", + "+ vlan-id 200;\n", + "+ }\n", + "+ frontend {\n", + "+ vlan-id 100;\n", + "+ }\n", + "+ }\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py --filter site=cmh --commit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we run the tool again it should report no changes:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run configure.py --filter site=cmh --no-commit" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/2_simple_tooling/configure.py b/examples/2_simple_tooling/configure.py index f0a16632..82b75744 100755 --- a/examples/2_simple_tooling/configure.py +++ b/examples/2_simple_tooling/configure.py @@ -1,53 +1,76 @@ #!/usr/bin/env python """ -In this example we write a CLI tool with brigade and click to deploy configuration. +Tool to configure datacenter """ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade from brigade.plugins.tasks import data, networking, text import click def configure(task): + """ + This function groups all the tasks needed to configure the + network: + + 1. Loading extra data + 2. Templates to build configuration + 3. Deploy configuration on the device + """ r = task.run(text.template_file, + name="Base Configuration", template="base.j2", path="../templates/{brigade_nos}") + # r.result holds the result of rendering the template + # we store in the host itself so we can keep updating + # it as we render other templates task.host["config"] = r.result r = task.run(data.load_yaml, + name="Loading extra data", file="../extra_data/{host}/l3.yaml") + # r.result holds the data contained in the yaml files + # we load the data inside the host itself for further use task.host["l3"] = r.result r = task.run(text.template_file, + name="Interfaces Configuration", template="interfaces.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result r = task.run(text.template_file, + name="Routing Configuration", template="routing.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result r = task.run(text.template_file, + name="Role-specific Configuration", template="{role}.j2", path="../templates/{brigade_nos}") + # we update our hosts' config task.host["config"] += r.result - return task.run(networking.napalm_configure, - replace=False, - configuration=task.host["config"]) + task.run(networking.napalm_configure, + name="Loading Configuration on the device", + replace=False, + configuration=task.host["config"]) @click.command() -@click.option('--filter', '-f', multiple=True) -@click.option('--commit/--no-commit', '-c', default=False) +@click.option('--filter', '-f', multiple=True, + help="k=v pairs to filter the devices") +@click.option('--commit/--no-commit', '-c', default=False, + help="whether you want to commit the changes or not") def main(filter, commit): - brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=not commit, - config=Config(raise_on_error=False), + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=not commit, + raise_on_error=False, ) # filter is going to be a list of key=value so we clean that first @@ -56,13 +79,17 @@ def main(filter, commit): k, v = f.split("=") filter_dict[k] = v - filtered = brigade.filter(**filter_dict) # let's filter the devices + # let's filter the devices + filtered = brg.filter(**filter_dict) results = filtered.run(task=configure) filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + skipped=True, + ) if __name__ == "__main__": diff --git a/examples/2_simple_tooling/get_facts.ipynb b/examples/2_simple_tooling/get_facts.ipynb new file mode 100644 index 00000000..87049422 --- /dev/null +++ b/examples/2_simple_tooling/get_facts.ipynb @@ -0,0 +1,306 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Get facts\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Very simple tool to get facts and print them on the screen.\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import networking, text\n",
+       " 7 \n",
+       " 8 import click\n",
+       " 9 \n",
+       "10 \n",
+       "11 @click.command()\n",
+       "12 @click.option('--filter', '-f', multiple=True,\n",
+       "13               help="k=v pairs to filter the devices")\n",
+       "14 @click.option('--get', '-g', multiple=True,\n",
+       "15               help="getters you want to use")\n",
+       "16 def main(filter, get):\n",
+       "17     """\n",
+       "18     Retrieve information from network devices using napalm\n",
+       "19     """\n",
+       "20     brg = easy_brigade(\n",
+       "21             host_file="../inventory/hosts.yaml",\n",
+       "22             group_file="../inventory/groups.yaml",\n",
+       "23             dry_run=False,\n",
+       "24             raise_on_error=False,\n",
+       "25     )\n",
+       "26 \n",
+       "27     # filter is going to be a list of key=value so we clean that first\n",
+       "28     filter_dict = {"type": "network_device"}\n",
+       "29     for f in filter:\n",
+       "30         k, v = f.split("=")\n",
+       "31         filter_dict[k] = v\n",
+       "32 \n",
+       "33     # select which devices we want to work with\n",
+       "34     filtered = brg.filter(**filter_dict)\n",
+       "35     results = filtered.run(networking.napalm_get,\n",
+       "36                            getters=get)\n",
+       "37 \n",
+       "38     # Let's print the result on screen\n",
+       "39     filtered.run(text.print_result,\n",
+       "40                  num_workers=1,  # task should be done synchronously\n",
+       "41                  data=results)\n",
+       "42 \n",
+       "43 \n",
+       "44 if __name__ == "__main__":\n",
+       "45     main()\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file get_facts.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "As usual, let's start with the help:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Usage: get_facts.py [OPTIONS]\n", + "\n", + " Retrieve information from network devices using napalm\n", + "\n", + "Options:\n", + " -f, --filter TEXT k=v pairs to filter the devices\n", + " -g, --get TEXT getters you want to use\n", + " --help Show this message and exit.\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run get_facts.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looks like we can use any getter. Let's see:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516038050.9974556\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516037549.5002303\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516037563.4058475\u001b[0m,\n", + " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", + " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run get_facts.py --filter name=spine00.cmh -g interfaces" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine00.cmh.cmh.acme.com'\u001b[0m,\n", + " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", + " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", + " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", + " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'uptime'\u001b[0m: \u001b[0m1156\u001b[0m,\n", + " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'users'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'admin'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'level'\u001b[0m: \u001b[0m15\u001b[0m,\n", + " \u001b[0m'password'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'role'\u001b[0m: \u001b[0m'network-admin'\u001b[0m,\n", + " \u001b[0m'sshkeys'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m,\n", + " \u001b[0m'vagrant'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'level'\u001b[0m: \u001b[0m15\u001b[0m,\n", + " \u001b[0m'password'\u001b[0m: \u001b[0m''\u001b[0m,\n", + " \u001b[0m'role'\u001b[0m: \u001b[0m'network-admin'\u001b[0m,\n", + " \u001b[0m'sshkeys'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run get_facts.py --filter name=spine00.cmh -g facts -g users" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/2_simple_tooling/get_facts.py b/examples/2_simple_tooling/get_facts.py index b8629905..8bbe60e0 100755 --- a/examples/2_simple_tooling/get_facts.py +++ b/examples/2_simple_tooling/get_facts.py @@ -1,15 +1,8 @@ #!/usr/bin/env python """ -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. - -The main difference with get_facts_simple.py is that instead of calling a plugin directly -we wrap it in a function. It is not very useful or necessary here but illustrates how -tasks can be grouped. +Very simple tool to get facts and print them on the screen. """ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade from brigade.plugins.tasks import networking, text import click @@ -24,10 +17,11 @@ def main(filter, get): """ Retrieve information from network devices using napalm """ - brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=True, - config=Config(raise_on_error=False), + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=False, ) # filter is going to be a list of key=value so we clean that first @@ -36,13 +30,14 @@ def main(filter, get): k, v = f.split("=") filter_dict[k] = v - filtered = brigade.filter(**filter_dict) # let's filter the devices + # select which devices we want to work with + filtered = brg.filter(**filter_dict) results = filtered.run(networking.napalm_get, getters=get) # Let's print the result on screen filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously + num_workers=1, # task should be done synchronously data=results) diff --git a/examples/2_simple_tooling/rollback.ipynb b/examples/2_simple_tooling/rollback.ipynb new file mode 100644 index 00000000..e09fc848 --- /dev/null +++ b/examples/2_simple_tooling/rollback.ipynb @@ -0,0 +1,411 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Rollback\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Tool to rollback configuration from a saved configuration\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import networking, text\n",
+       " 7 \n",
+       " 8 import click\n",
+       " 9 \n",
+       "10 \n",
+       "11 def rollback(task, path):\n",
+       "12     """\n",
+       "13     This function loads the backup from ./$path/$hostname and\n",
+       "14     deploys it.\n",
+       "15     """\n",
+       "16     task.run(networking.napalm_configure,\n",
+       "17              name="Loading Configuration on the device",\n",
+       "18              replace=True,\n",
+       "19              filename="{}/{}".format(path, task.host))\n",
+       "20 \n",
+       "21 \n",
+       "22 @click.command()\n",
+       "23 @click.option('--filter', '-f', multiple=True,\n",
+       "24               help="k=v pairs to filter the devices")\n",
+       "25 @click.option('--commit/--no-commit', '-c', default=False,\n",
+       "26               help="whether you want to commit the changes or not")\n",
+       "27 @click.option('--path', '-p', default=".",\n",
+       "28               help="Where to save the backup files")\n",
+       "29 def main(filter, commit, path):\n",
+       "30     brg = easy_brigade(\n",
+       "31             host_file="../inventory/hosts.yaml",\n",
+       "32             group_file="../inventory/groups.yaml",\n",
+       "33             dry_run=not commit,\n",
+       "34             raise_on_error=True,\n",
+       "35     )\n",
+       "36 \n",
+       "37     # filter is going to be a list of key=value so we clean that first\n",
+       "38     filter_dict = {"type": "network_device"}\n",
+       "39     for f in filter:\n",
+       "40         k, v = f.split("=")\n",
+       "41         filter_dict[k] = v\n",
+       "42 \n",
+       "43     # let's filter the devices\n",
+       "44     filtered = brg.filter(**filter_dict)\n",
+       "45 \n",
+       "46     results = filtered.run(task=rollback, path=path)\n",
+       "47 \n",
+       "48     filtered.run(text.print_result,\n",
+       "49                  num_workers=1,  # task should be done synchronously\n",
+       "50                  data=results,\n",
+       "51                  task_id=-1,  # we only want to print the last task\n",
+       "52                  )\n",
+       "53 \n",
+       "54 \n",
+       "55 if __name__ == "__main__":\n",
+       "56     main()\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file rollback.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "As usual, let's start with the help:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Usage: rollback.py [OPTIONS]\n", + "\n", + "Options:\n", + " -f, --filter TEXT k=v pairs to filter the devices\n", + " -c, --commit / --no-commit whether you want to commit the changes or not\n", + " -p, --path TEXT Where to save the backup files\n", + " --help Show this message and exit.\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looks like we can filter devices as usual, we can test changes as with the ``configure.py`` tool and that we can even choose the path where to look for the configurations. Let's try it:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -15,10 +15,10 @@\n", + " !\n", + " aaa authorization exec default local\n", + " !\n", + "-aaa root secret sha512 $6$DInx3sLBwR.ikKs8$ThNQ/uVPDT9YXcreGqYZ53IZA.abXsMPsV4jdylCVl5Nu6GMcVcFgvE9L3hvM/vZDJS7.1xs9ZwphFdP1BhNP1\n", + "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", + " !\n", + "-username admin privilege 15 role network-admin secret sha512 $6$JHsj9QpFAuo8TKNS$98Wmnr/.L2CCHtd6peL2q4fGOnt39C/XtBPJms6J/u1qBX9xWvf99FIYuQPSoqCTYBrN0ZNjzVKeIkNnV.Gez.\n", + "-username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", + "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + " !\n", + " interface Ethernet1\n", + " description link to leaf00.cmh\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system root-authentication]\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\"; ## SECRET-DATA\n", + "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcn03wv4SesN3JeAdwiOzgEdY5f+u4yptRK8OEjkHLuJg/6lR6MoD2BkdvWLShtx97/kVbxbTWOu9XM1mZ+E/YDR0mt7eHWwiy/OlgP9i0MzSj+XhtMUzRp7Ow+34VrrW7yQmuIkigq/QkDPv3b6O0u0y6azCQVrg5pvwRdZU2xTyKt/aM6/TL+glVh508XqG7RzlsmIRnrSa0WfHzcbQKPTJXlAjLGoYk53SltxW//e5HMQnTAJop0ic7FniXrVhS8F9iKxfLfFqzB5JJ2gaQX3y3cPr1MIg60aoSprI/8297wjE6fnQGcp1H1fD5rJx96m+3ViwydbtElhljcreB vagrant\"; ## SECRET-DATA\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -15,10 +15,10 @@\n", + " !\n", + " aaa authorization exec default local\n", + " !\n", + "-aaa root secret sha512 $6$8FLl5NfSda1Wh6d8$3SO4hP73eJnMf5kHrKUoMJ4jMtLU7iRrj/FJwAgAdk4GImfqy6WYLqMgmfpYOe6v/T4rjIFpOX..LmhCnbgbO0\n", + "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", + " !\n", + "-username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", + "-username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", + "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + " !\n", + " vlan 100\n", + " name frontend\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system root-authentication]\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\"; ## SECRET-DATA\n", + "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDuoXCl14JaKfWnyKSp1c4wjBv6XiCMsIRT7w0+BQxvaS7D1AoxNksYCTTjAJ8HVaMcLD7MI4bajS3/oEwtmCVpNJBG91UCi0P3tN2GjQwCwzrZG0eNpP2Gy51sKcq2lM1sxi+9QKYAtK5gmqV2Y8UeOuo4jKVNxCrPLYXO2BQBGCBUPayDjiPDir0H2BCKGpuwgegHgpkFKw+tWqo0IFsQmnvOQX+mjGDV8PVghCnzLO2ZbZrZPu5rRSgZm+CFGK1DGDsPBgdElxnu6ytjVIKkDzHrZ6HEm7yFgneb0WDGEmVl8MvBS9VPXXv8NzHJTUnedbxWKcqJ+xurpAGAYm6n vagrant\"; ## SECRET-DATA\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py --path backups/cmh --no-commit --filter site=cmh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looks legit, let's commit the changes:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -15,10 +15,10 @@\n", + " !\n", + " aaa authorization exec default local\n", + " !\n", + "-aaa root secret sha512 $6$DInx3sLBwR.ikKs8$ThNQ/uVPDT9YXcreGqYZ53IZA.abXsMPsV4jdylCVl5Nu6GMcVcFgvE9L3hvM/vZDJS7.1xs9ZwphFdP1BhNP1\n", + "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", + " !\n", + "-username admin privilege 15 role network-admin secret sha512 $6$JHsj9QpFAuo8TKNS$98Wmnr/.L2CCHtd6peL2q4fGOnt39C/XtBPJms6J/u1qBX9xWvf99FIYuQPSoqCTYBrN0ZNjzVKeIkNnV.Gez.\n", + "-username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", + "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", + " !\n", + " interface Ethernet1\n", + " description link to leaf00.cmh\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system root-authentication]\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\"; ## SECRET-DATA\n", + "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcn03wv4SesN3JeAdwiOzgEdY5f+u4yptRK8OEjkHLuJg/6lR6MoD2BkdvWLShtx97/kVbxbTWOu9XM1mZ+E/YDR0mt7eHWwiy/OlgP9i0MzSj+XhtMUzRp7Ow+34VrrW7yQmuIkigq/QkDPv3b6O0u0y6azCQVrg5pvwRdZU2xTyKt/aM6/TL+glVh508XqG7RzlsmIRnrSa0WfHzcbQKPTJXlAjLGoYk53SltxW//e5HMQnTAJop0ic7FniXrVhS8F9iKxfLfFqzB5JJ2gaQX3y3cPr1MIg60aoSprI/8297wjE6fnQGcp1H1fD5rJx96m+3ViwydbtElhljcreB vagrant\"; ## SECRET-DATA\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m@@ -15,10 +15,10 @@\n", + " !\n", + " aaa authorization exec default local\n", + " !\n", + "-aaa root secret sha512 $6$8FLl5NfSda1Wh6d8$3SO4hP73eJnMf5kHrKUoMJ4jMtLU7iRrj/FJwAgAdk4GImfqy6WYLqMgmfpYOe6v/T4rjIFpOX..LmhCnbgbO0\n", + "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", + " !\n", + "-username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", + "-username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", + "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", + "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", + " !\n", + " vlan 100\n", + " name frontend\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", + "\u001b[0m[edit system root-authentication]\n", + "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\"; ## SECRET-DATA\n", + "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDuoXCl14JaKfWnyKSp1c4wjBv6XiCMsIRT7w0+BQxvaS7D1AoxNksYCTTjAJ8HVaMcLD7MI4bajS3/oEwtmCVpNJBG91UCi0P3tN2GjQwCwzrZG0eNpP2Gy51sKcq2lM1sxi+9QKYAtK5gmqV2Y8UeOuo4jKVNxCrPLYXO2BQBGCBUPayDjiPDir0H2BCKGpuwgegHgpkFKw+tWqo0IFsQmnvOQX+mjGDV8PVghCnzLO2ZbZrZPu5rRSgZm+CFGK1DGDsPBgdElxnu6ytjVIKkDzHrZ6HEm7yFgneb0WDGEmVl8MvBS9VPXXv8NzHJTUnedbxWKcqJ+xurpAGAYm6n vagrant\"; ## SECRET-DATA\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py --path backups/cmh --commit --filter site=cmh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And let's verify all changes were applied correctly:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run rollback.py --path backups/cmh --no-commit --filter site=cmh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/2_simple_tooling/rollback.py b/examples/2_simple_tooling/rollback.py new file mode 100755 index 00000000..175d7dd5 --- /dev/null +++ b/examples/2_simple_tooling/rollback.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +""" +Tool to rollback configuration from a saved configuration +""" +from brigade.easy import easy_brigade +from brigade.plugins.tasks import networking, text + +import click + + +def rollback(task, path): + """ + This function loads the backup from ./$path/$hostname and + deploys it. + """ + task.run(networking.napalm_configure, + name="Loading Configuration on the device", + replace=True, + filename="{}/{}".format(path, task.host)) + + +@click.command() +@click.option('--filter', '-f', multiple=True, + help="k=v pairs to filter the devices") +@click.option('--commit/--no-commit', '-c', default=False, + help="whether you want to commit the changes or not") +@click.option('--path', '-p', default=".", + help="Where to save the backup files") +def main(filter, commit, path): + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=not commit, + raise_on_error=True, + ) + + # filter is going to be a list of key=value so we clean that first + filter_dict = {"type": "network_device"} + for f in filter: + k, v = f.split("=") + filter_dict[k] = v + + # let's filter the devices + filtered = brg.filter(**filter_dict) + + results = filtered.run(task=rollback, path=path) + + filtered.run(text.print_result, + num_workers=1, # task should be done synchronously + data=results, + task_id=-1, # we only want to print the last task + ) + + +if __name__ == "__main__": + main() diff --git a/examples/2_simple_tooling/validate.ipynb b/examples/2_simple_tooling/validate.ipynb new file mode 100644 index 00000000..75711ffc --- /dev/null +++ b/examples/2_simple_tooling/validate.ipynb @@ -0,0 +1,404 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Validate\n", + "\n", + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 #!/usr/bin/env python\n",
+       " 2 """\n",
+       " 3 Runbook that verifies that BGP sessions are configured and up.\n",
+       " 4 """\n",
+       " 5 from brigade.easy import easy_brigade\n",
+       " 6 from brigade.plugins.tasks import data, networking, text\n",
+       " 7 \n",
+       " 8 import click\n",
+       " 9 \n",
+       "10 \n",
+       "11 def validate(task):\n",
+       "12     task.host["config"] = ""\n",
+       "13 \n",
+       "14     r = task.run(name="read data",\n",
+       "15                  task=data.load_yaml,\n",
+       "16                  file="../extra_data/{host}/l3.yaml")\n",
+       "17 \n",
+       "18     validation_rules = [{\n",
+       "19         'get_bgp_neighbors': {\n",
+       "20             'global': {\n",
+       "21                 'peers': {\n",
+       "22                     '_mode': 'strict',\n",
+       "23                 }\n",
+       "24             }\n",
+       "25         }\n",
+       "26     }]\n",
+       "27     peers = validation_rules[0]['get_bgp_neighbors']['global']['peers']\n",
+       "28     for session in r.result['sessions']:\n",
+       "29         peers[session['ipv4']] = {'is_up': True}\n",
+       "30 \n",
+       "31     task.run(name="validating data",\n",
+       "32              task=networking.napalm_validate,\n",
+       "33              validation_source=validation_rules)\n",
+       "34 \n",
+       "35 \n",
+       "36 def print_compliance(task, results):\n",
+       "37     """\n",
+       "38     We use this task so we can access directly the result\n",
+       "39     for each specific host and see if the task complies or not\n",
+       "40     and pass it to print_result.\n",
+       "41     """\n",
+       "42     task.run(name="print result",\n",
+       "43              task=text.print_result,\n",
+       "44              data=results[task.host.name],\n",
+       "45              failed=not results[task.host.name][2].result['complies'],\n",
+       "46              )\n",
+       "47 \n",
+       "48 \n",
+       "49 @click.command()\n",
+       "50 @click.option('--filter', '-f', multiple=True,\n",
+       "51               help="k=v pairs to filter the devices")\n",
+       "52 def main(filter):\n",
+       "53     brg = easy_brigade(\n",
+       "54             host_file="../inventory/hosts.yaml",\n",
+       "55             group_file="../inventory/groups.yaml",\n",
+       "56             dry_run=False,\n",
+       "57             raise_on_error=True,\n",
+       "58     )\n",
+       "59 \n",
+       "60     # filter is going to be a list of key=value so we clean that first\n",
+       "61     filter_dict = {"type": "network_device"}\n",
+       "62     for f in filter:\n",
+       "63         k, v = f.split("=")\n",
+       "64         filter_dict[k] = v\n",
+       "65 \n",
+       "66     # select which devices we want to work with\n",
+       "67     filtered = brg.filter(**filter_dict)\n",
+       "68 \n",
+       "69     results = filtered.run(task=validate)\n",
+       "70 \n",
+       "71     filtered.run(print_compliance,\n",
+       "72                  results=results,\n",
+       "73                  num_workers=1)\n",
+       "74 \n",
+       "75 \n",
+       "76 if __name__ == "__main__":\n",
+       "77     main()\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file validate.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo\n", + "\n", + "As usual, let's start with the help:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Usage: validate.py [OPTIONS]\n", + "\n", + "Options:\n", + " -f, --filter TEXT k=v pairs to filter the devices\n", + " --help Show this message and exit.\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run validate.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Not much to it, very similar to its runbook counterpart:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", + " \u001b[0m'10.0.0.3'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", + " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", + " \u001b[0m'10.0.1.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", + " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", + " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", + " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", + "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", + " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", + " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", + " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "%run validate.py --filter site=cmh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/2_simple_tooling/validate.py b/examples/2_simple_tooling/validate.py index a64a928f..04597074 100755 --- a/examples/2_simple_tooling/validate.py +++ b/examples/2_simple_tooling/validate.py @@ -1,9 +1,8 @@ #!/usr/bin/env python """ -In this example we write a CLI tool with brigade and click to deploy configuration. +Runbook that verifies that BGP sessions are configured and up. """ -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory +from brigade.easy import easy_brigade from brigade.plugins.tasks import data, networking, text import click @@ -29,26 +28,33 @@ def validate(task): for session in r.result['sessions']: peers[session['ipv4']] = {'is_up': True} - return task.run(name="validating data", - task=networking.napalm_validate, - validation_source=validation_rules) + task.run(name="validating data", + task=networking.napalm_validate, + validation_source=validation_rules) def print_compliance(task, results): + """ + We use this task so we can access directly the result + for each specific host and see if the task complies or not + and pass it to print_result. + """ task.run(name="print result", task=text.print_result, data=results[task.host.name], - failed=not results[task.host.name].result['complies'], + failed=not results[task.host.name][2].result['complies'], ) @click.command() -@click.option('--filter', '-f', multiple=True) -@click.option('--commit/--no-commit', '-c', default=False) -def main(filter, commit): - brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=False, +@click.option('--filter', '-f', multiple=True, + help="k=v pairs to filter the devices") +def main(filter): + brg = easy_brigade( + host_file="../inventory/hosts.yaml", + group_file="../inventory/groups.yaml", + dry_run=False, + raise_on_error=True, ) # filter is going to be a list of key=value so we clean that first @@ -57,7 +63,8 @@ def main(filter, commit): k, v = f.split("=") filter_dict[k] = v - filtered = brigade.filter(**filter_dict) # let's filter the devices + # select which devices we want to work with + filtered = brg.filter(**filter_dict) results = filtered.run(task=validate) diff --git a/examples/highlighter.py b/examples/highlighter.py new file mode 100644 index 00000000..1e1c53fe --- /dev/null +++ b/examples/highlighter.py @@ -0,0 +1,34 @@ +from __future__ import print_function + +from IPython.core.magic import register_line_magic +from IPython.display import HTML + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_for_filename, get_lexer_by_name + + +HTML_TEMPLATE = """ +{} +""" + + +@register_line_magic +def highlight_file(filename): + lexer = get_lexer_by_name("py3") + + linenos = "inline" + + formatter = HtmlFormatter(style='default', + cssclass='pygments', + linenos=linenos) + + with open(filename) as f: + code = f.read() + + html_code = highlight(code, lexer, formatter) + css = formatter.get_style_defs() + + return HTML(HTML_TEMPLATE.format(css, html_code)) From 7e1bb533df1d0183ae180eaec7284097ed494bc6 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sat, 20 Jan 2018 17:44:48 +0100 Subject: [PATCH 30/31] minor fixes proposed in the comments --- brigade/core/task.py | 2 +- brigade/easy.py | 2 +- brigade/plugins/tasks/files/write.py | 8 ++--- docs/tutorials/intro/brigade.rst | 4 +-- docs/tutorials/intro/inventory.rst | 34 ++++++++++-------- docs/tutorials/intro/running_tasks_errors.rst | 8 ++--- examples/1_simple_runbooks/rollback.py | 3 +- examples/highlighter.py | 2 +- examples/inventory/groups.yaml | 1 - tests/inventory_data/nsot/nsot.sqlite3 | Bin 245760 -> 251904 bytes 10 files changed, 34 insertions(+), 30 deletions(-) diff --git a/brigade/core/task.py b/brigade/core/task.py index a1af8ad4..baa49813 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -20,7 +20,7 @@ class Task(object): name (``string``): name of task, defaults to ``task.__name__`` skipped (``bool``): whether to run hosts that should be skipped otherwise or not params: Parameters that will be passed to the ``task``. - self.results (:obj:`brigade.core.tasks.MultiResult`): Intermediate results + self.results (:obj:`brigade.core.task.MultiResult`): Intermediate results host (:obj:`brigade.core.inventory.Host`): Host we are operating with. Populated right before calling the ``task`` brigade(:obj:`brigade.core.Brigade`): Populated right before calling diff --git a/brigade/easy.py b/brigade/easy.py index f05ba6aa..8726e9a5 100644 --- a/brigade/easy.py +++ b/brigade/easy.py @@ -3,7 +3,7 @@ from brigade.plugins.inventory.simple import SimpleInventory -def easy_brigade(host_file="host.yaml", group_file="groups.yaml", dry_run=True, **kwargs): +def easy_brigade(host_file="host.yaml", group_file="groups.yaml", dry_run=False, **kwargs): """ Helper function to create easily a :obj:`brigade.core.Brigade` object. diff --git a/brigade/plugins/tasks/files/write.py b/brigade/plugins/tasks/files/write.py index 6009e28a..4bc28d4e 100644 --- a/brigade/plugins/tasks/files/write.py +++ b/brigade/plugins/tasks/files/write.py @@ -4,15 +4,15 @@ from brigade.core.task import Result -def read_file(file): +def _read_file(file): if not os.path.exists(file): return [] with open(file, "r") as f: return f.read().splitlines() -def generate_diff(filename, content, append): - original = read_file(filename) +def _generate_diff(filename, content, append): + original = _read_file(filename) if append: c = list(original) c.extend(content.splitlines()) @@ -38,7 +38,7 @@ def write(task, filename, content, append=False): * changed (``bool``): * diff (``str``): unified diff """ - diff = generate_diff(filename, content, append) + diff = _generate_diff(filename, content, append) if not task.dry_run: mode = "a+" if append else "w+" diff --git a/docs/tutorials/intro/brigade.rst b/docs/tutorials/intro/brigade.rst index ae5f7c10..2a5fb807 100644 --- a/docs/tutorials/intro/brigade.rst +++ b/docs/tutorials/intro/brigade.rst @@ -11,8 +11,8 @@ Using the "raw" API If you want to use the "raw" API you need two things: -1. A configuration object. -2. An inventory object. +1. A :obj:`brigade.core.configuration.Config` object. +2. An :doc:`inventory ` object. Once you have them, you can create the brigade object yourself. For example:: diff --git a/docs/tutorials/intro/inventory.rst b/docs/tutorials/intro/inventory.rst index 5cd1b481..a8f089cb 100644 --- a/docs/tutorials/intro/inventory.rst +++ b/docs/tutorials/intro/inventory.rst @@ -3,11 +3,28 @@ The Inventory The inventory is arguably the most important piece of Brigade. The inventory organizes hosts and makes sure tasks have the correct data for each host. + +Inventory data +-------------- + +Before we start let's take a look at the inventory data: + +* ``hosts.yaml`` + +.. literalinclude:: ../../../examples/inventory/hosts.yaml + +* ``groups.yaml`` + +.. literalinclude:: ../../../examples/inventory/groups.yaml + +Loading the inventory +--------------------- + You can create the inventory in different ways, depending on your data source. To see the available plugins you can use go to the :ref:`ref-inventory` reference guide. -.. note:: For this and the subsequent sections of this tutorial we are going to use the :obj:`SimpleInventory ` with the data located in ``/examples/inventory/``. We will also use the ``Vagrantfile`` located there so you should be able to reproduce everything. You can head to `Hosts/Groups contents`_ to see the contents of the file just for reference. +.. note:: For this and the subsequent sections of this tutorial we are going to use the :obj:`SimpleInventory ` with the data located in ``/examples/inventory/``. We will also use the ``Vagrantfile`` located there so you should be able to reproduce everything. -First, let's create the inventory:: +First, let's load the inventory:: >>> from brigade.plugins.inventory.simple import SimpleInventory >>> inventory = SimpleInventory(host_file="hosts.yaml", group_file="groups.yaml") @@ -116,16 +133,3 @@ You can also do more complex filtering by using functions or lambdas:: dict_keys(['host1.cmh', 'host2.cmh', 'host1.bma', 'host2.bma']) Not the most useful example but it should be enough to illustrate how it works. - - -Hosts/Groups contents ---------------------- - - -* ``hosts.yaml`` - -.. literalinclude:: ../../../examples/inventory/hosts.yaml - -* ``groups.yaml`` - -.. literalinclude:: ../../../examples/inventory/groups.yaml diff --git a/docs/tutorials/intro/running_tasks_errors.rst b/docs/tutorials/intro/running_tasks_errors.rst index 1ef3d1ac..7e34e1cc 100644 --- a/docs/tutorials/intro/running_tasks_errors.rst +++ b/docs/tutorials/intro/running_tasks_errors.rst @@ -1,10 +1,10 @@ Dealing with task errors ======================== -Tasks can fail due to many reasons. A continuation we will see how to deal with errors effectively with brigade. +Tasks can fail due to many reasons. As we continue we will see how to deal with errors effectively with brigade. -Failing by default ------------------- +Failing on error by default +--------------------------- Brigade can raise a :obj:`brigade.core.exceptions.BrigadeExecutionError` exception automatically as soon as an error occurs. For instance:: @@ -367,7 +367,7 @@ Regardless of the default behavior you can force ``raise_on_error`` on a per tas ... raise_on_error=False) >>> -As you can see, regardless of what ``brigade`` had configured to do, the task failed on the first case but didn't on the second one. +As you can see, regardless of what ``brigade`` had been configured to do, the task failed on the first case but didn't on the second one. Which one to use ---------------- diff --git a/examples/1_simple_runbooks/rollback.py b/examples/1_simple_runbooks/rollback.py index 655f07ac..2c219cc7 100755 --- a/examples/1_simple_runbooks/rollback.py +++ b/examples/1_simple_runbooks/rollback.py @@ -32,7 +32,6 @@ def main(filter, get): raise_on_error=True, ) - # select which devices we want to work with filtered = brg.filter(type="network_device", site="cmh") @@ -43,5 +42,7 @@ def main(filter, get): data=results, task_id=-1, # we only want to print the last task ) + + if __name__ == "__main__": main() diff --git a/examples/highlighter.py b/examples/highlighter.py index 1e1c53fe..ebe31b33 100644 --- a/examples/highlighter.py +++ b/examples/highlighter.py @@ -5,7 +5,7 @@ from pygments import highlight from pygments.formatters import HtmlFormatter -from pygments.lexers import get_lexer_for_filename, get_lexer_by_name +from pygments.lexers import get_lexer_by_name HTML_TEMPLATE = """