From 62c1f10e7b99d5db6bc4352cb72b7e96131a4c8f Mon Sep 17 00:00:00 2001 From: Alexander Tivelkov Date: Tue, 17 Mar 2015 17:25:19 +0300 Subject: [PATCH] Initial implementation of Plugable Classes Adds a PluginLoader which loads classes defined as stevedore plugins at io.murano.extension namespace and registers them as MuranoPL classes in class loader. Modifies the ClientManager class to make the _get_client method public, so other code may use it to add custom clients. This is useful for plugins which may define their own clients. Modifies the configuration settings adding 'enabled_plugins' parameter to control which of the installed plugins are active. Adds an example plugin which encapsulates Glance interaction logic to: * List all available glance images * Get Image by ID * Get Image by Name * Output image info with murano-related metadata Adds a demo application which demonstrates the usage of plugin. The app consist of the following components: * An 'ImageValidatorMixin' class which inherits generic instance class (io.murano.resources.Instance) and adds a method capable to validate Instance's image for having appropriate murano metadata type. This class may be used as a mixin when added to inheritance hierarchy of concrete instance classes. * A concrete class called DemoInstance which inherits from io.murano.resources.LinuxMuranoInstance and ImageValidatorMixin to add the image validation logic to standard Murano-enabled Linux-based instance. * An application which deploys a single VM using the DemoInstance class if the tag on user-supplied image matches the user-supplied constant. The ImageValidatorMixin demonstrates the instantiation of plugin-provided class and its usage, as well as handling of exception which may be thrown if the plugin is not installed in the environment. Change-Id: I978339d87033bbe38dad4c2102612d8f3a1eb3c3 Implements-blueprint: plugable-classes --- .../Classes/DemoApp.yaml | 40 ++++++ .../Classes/DemoInstance.yaml | 15 ++ .../Classes/ImageValidatorMixin.yaml | 36 +++++ .../io.murano.apps.demo.DemoApp/UI/ui.yaml | 81 +++++++++++ .../io.murano.apps.demo.DemoApp/logo.png | Bin 0 -> 28939 bytes .../io.murano.apps.demo.DemoApp/manifest.yaml | 12 ++ .../murano_exampleplugin/__init__.py | 82 +++++++++++ .../murano_exampleplugin/cfg.py | 24 ++++ .../murano_exampleplugin/requiremenets.txt | 1 + .../plugins/murano_exampleplugin/setup.cfg | 18 +++ contrib/plugins/murano_exampleplugin/setup.py | 20 +++ murano/common/config.py | 7 +- murano/common/engine.py | 12 ++ murano/common/plugin_loader.py | 128 ++++++++++++++++++ murano/engine/client_manager.py | 14 +- 15 files changed, 482 insertions(+), 8 deletions(-) create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/logo.png create mode 100644 contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/manifest.yaml create mode 100644 contrib/plugins/murano_exampleplugin/murano_exampleplugin/__init__.py create mode 100644 contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py create mode 100644 contrib/plugins/murano_exampleplugin/requiremenets.txt create mode 100644 contrib/plugins/murano_exampleplugin/setup.cfg create mode 100644 contrib/plugins/murano_exampleplugin/setup.py mode change 100644 => 100755 murano/common/engine.py create mode 100644 murano/common/plugin_loader.py diff --git a/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml new file mode 100644 index 000000000..44a4300c6 --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml @@ -0,0 +1,40 @@ +Namespaces: + =: io.murano.apps.example.plugin + std: io.murano + res: io.murano.resources + sys: io.murano.system + + +Name: DemoApp + +Extends: std:Application + +Properties: + name: + Contract: $.string().notNull() + + instance: + Contract: $.class(res:Instance).notNull() + +Workflow: + initialize: + Body: + - $.environment: $.find(std:Environment).require() + + deploy: + Body: + - If: !yaql "not bool($.getAttr(deployed))" + Then: + - $this.find(std:Environment).reporter.report($this, 'Creating VM ') + - $securityGroupIngress: + - ToPort: 22 + FromPort: 22 + IpProtocol: tcp + External: True + - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) + - $.instance.deploy() + - $resources: new(sys:Resources) + - $this.find(std:Environment).reporter.report($this, 'Test VM is installed') + - $.host: $.instance.ipAddresses[0] + - $.user: 'root' + - $.setAttr(deployed, True) diff --git a/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml new file mode 100644 index 000000000..5b6fdc6a6 --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml @@ -0,0 +1,15 @@ +Namespaces: + =: io.murano.apps.example.plugin + res: io.murano.resources + +Name: DemoInstance + +Extends: + - res:LinuxMuranoInstance + - ImageValidatorMixin + +Workflow: + deploy: + Body: + - $.validateImage() + - $.super($.deploy()) diff --git a/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml new file mode 100644 index 000000000..0ad0e889f --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml @@ -0,0 +1,36 @@ +Namespaces: + =: io.murano.apps.example.plugin + res: io.murano.resources + +Name: ImageValidatorMixin + +Extends: + - res:Instance + +Properties: + requiredType: + Contract: $.string().notNull() + +Workflow: + validateImage: + Body: + - Try: + - $glance: new('io.murano.extensions.mirantis.example.Glance') + Catch: + With: 'murano.dsl.exceptions.NoPackageForClassFound' + Do: + Throw: PluginNotFoundException + Message: 'Plugin for interaction with Glance is not installed' + - $glanceImage: $glance.getById($.image) + - If: $glanceImage = null + Then: + Throw: ImageNotFoundException + Message: 'Image with specified Id was not found' + - If: $glanceImage.meta = null + Then: + Throw: InvalidImageException + Message: 'Image does not contain Murano metadata tag' + - If: $glanceImage.meta.type != $.requiredType + Then: + Throw: InvalidImageException + Message: 'Image has unappropriate Murano type' diff --git a/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml new file mode 100644 index 000000000..5e374b698 --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml @@ -0,0 +1,81 @@ +Version: 2 + +Application: + ?: + type: io.murano.apps.example.plugin.DemoApp + name: $.appConfiguration.name + instance: + ?: + type: io.murano.apps.example.plugin.DemoInstance + name: generateHostname($.instanceConfiguration.unitNamingPattern, 1) + flavor: $.instanceConfiguration.flavor + image: $.instanceConfiguration.osImage + requiredType: $.appConfiguration.requiredType + assignFloatingIp: $.appConfiguration.assignFloatingIP + keyname: $.instanceConfiguration.keyPair + +Forms: + - appConfiguration: + fields: + - name: name + type: string + label: Application Name + initial: Demo + description: >- + Enter a desired name for the application. Just A-Z, a-z, 0-9, dash and + underline are allowed + - name: requiredType + type: string + label: Required MuranoImage Type + initial: linux + description: >- + Enter a value to be matched against 'type' field of MuranoImage metadata + - name: assignFloatingIP + type: boolean + label: Assign Floating IP + description: >- + Select to true to assign floating IP automatically + initial: false + required: false + widgetMedia: + css: {all: ['muranodashboard/css/checkbox.css']} + - instanceConfiguration: + fields: + - name: title + type: string + required: false + hidden: true + description: Specify some instance parameters on which the application would be created + - name: flavor + type: flavor + label: Instance flavor + description: >- + Select registered in Openstack flavor. Consider that application performance + depends on this parameter. + required: false + - name: osImage + type: image + imageType: linux + label: Instance image + description: >- + Select a valid image for the application. Image should already be prepared and + registered in glance. + - name: keyPair + type: keypair + label: Key Pair + description: >- + Select a Key Pair to control access to instances. You can login to + instances using this KeyPair after the deployment of application. + required: false + - name: availabilityZone + type: azone + label: Availability zone + description: Select availability zone where the application would be installed. + required: false + - name: unitNamingPattern + label: Hostname + type: string + required: false + widgetMedia: + js: ['muranodashboard/js/support_placeholder.js'] + css: {all: ['muranodashboard/css/support_placeholder.css']} diff --git a/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/logo.png b/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..165bf1eef01688bf42c96e869f2fd2aac3a86e1e GIT binary patch literal 28939 zcmZU4by!nh-1zA36afVZX{05UZWN>$ozfBmM}BEhLb^ptYIKemT~Z=UVx*M7C}AMs zd-47K@jlOQ&$HcU_uO;Nr|W!f+zVYbGGZoT5C}x3p{}eC0%2VM-&I0<;EqOhXdG}M z@=`bT0f7V`-F>kFN)-G-AYwfiC8ZZHoIQO#eVjeL9%?8lJ@k6x>FDC-00IRpB~bES#9b3q(oSV9(}r^9T!c5h5$g$BrQA0$~NrkPw0pIyneo=9s?}zZA19 zu)-p-5WdM$T7+S=pl5Fr6iY$RRI$SHGPw*upKw9eV|KP1pceulYwpmk8Bkc>MHUnb zWS+`KhxI8L^pM6ORvF|h1FD)ddZz+15eCsZYV|(_{SW{NYZyDKgKAqqhzUxPdJrKg zNcd%J6b}d&3bG!4^e6xnnFXR%Ju#L&=7Ujg^8%F0s+Vo!c&ZX^PAK3_U~DYN`glTv zfsakm*}~p$4Wf8T>H!+s~Y6TxUy?Pkxwq$9jrqO`uP^3&y8jceGI{Olq5- z-~9RWhi1Q`xy`6?@U2s?6~g$&?J`968gsVVwayVP0uEQhy;$x2`dhn%dGaGsr0vpH zqS|!}$=_?18TNh++cpzk+Krdg-U-Ut?@vWa*y9vG@(gf(`)j|x!heM$zakE*eJzg# zO&0h9w!=Bs;-)n5MXX~$pcA-f&-cfKIAP8aYm<<_hYB}ppEyBbjv61mK_E+I4#8KW z4GKeqAdvE>aNgSI_kZ^=@+0t>d+?TeNUp6VA{9CMdle}aiS5If-&pfh{ipaidc2l} z*IMZNGZrz#OXuhmFLJ?N!&Y)RFN!}lg!w&>JHm-@pAQq#*sv|cV%fekd6YoNkQH^o z)}xL~9nbM_B$oCiyH>Ke`V;+kuO1qzvz*Ai!G0QJq1v7-GYk^>?>f3e?MZ5Q{Y$y; zq)kd~UnH3#RX>wV+50}pPNe^oGxesCN$$Nw;r3LeGsQxpywZ>`Vuz7HwBJowaIjCY zPKuu>#cr&Qy^6GQkg3+F3I`r{W;a;}!KDsYK@dscLKGj`us&Be(KgZjq5kC|Glv+} z0+A#UPUOp8ZuXQfT8&)oRG0mXwjxhrOw{=w%~2=P2vI^~mHH()sNrqm+>ad)In`r>Xo8`G%u@=KbvZDf;vNPqq_t;yl}D-o+ON z>yPzEUu)jmq}&wS#COOPd2U#gt&jRr_=-wA(M++ruvx$6Wy}{EF~97m;}P^4S|!z= z(`J~Q8JsaLH9NlTRH!so-Ha*YZ)3Y^I~hATzagY#(R2|*u}+Wg>1oQa4MJi5>w&y#q*Uyafs#MqEa(BPV?Z6RdD8xnBrZhID(Bp)>QlEW`4(JeEi$=nTDE9b z^@A$KdzV3r4UP@>LHEHTE_^OFE-js3I_PYDowib*QW#$B%$}N-m>8OY8&s?FsyXN2)$lT^vNqim z-OBtqr@q?b=8@*|mV33EKkye}&F;;?Hcqw-PAvnlK6FZFN^pDdhZ{KpvY2`DN)|Jg z5TC|3w9-b>%h?mTXocW`-|4$TP9~F-lRrc^nrX&(`d#u2a%R+=7@XRCB2HuDko%(*{^8t(k5>3il4UVHoPEH9@ar}yB?VhvgX?ShV3D)=f^z>{w(>NC|hthD-a zb6SCENiW=~Y5S3oolvrIHL|v)1X6*qIb@JOmw!{RA-^Fjxh%45YV}nT z(K6gJrq}10Zxywo6#D3|{ol8Lr$J?5oLEjcapBLy&9Sd8-UhLCCn!iSI-J+fA3T5m zsqH!4^ThwE|J&@TW^*3iPpW3aY-dg+Pq>gry$IQvnNbrg^ zVVZuDAr6^dQu0|*{-d0(yqx3oMDJ;hfTlRFe1+g^*#*gVv37+)K}XZCvnF{a8g5$5 zPX&2l5*}_{E7u37G3$BjBpXnMOrc@0S76~p@9(HG`f@>Pe(CgQ+OfJ)sYlN#dB4!V z=RBc-a3&B3_OkRku?tO0OvnCEvfyRaJyGY?dHZocxkEgVV?eZof3q!;yF95wn@k&( zBb`s5?f=kSL{7T%NyNwGByYtEHl37ar(aH6j{aj+4pH^D10L}$Lab>7Bd!feRd}WX z#LbGOJFmIy}B;Bm64a)_2Xk?z4K82gVt&kWQ|*I{6Ztyic#ai*}2N1P{#Pgv*WLSI22!KY4TB#QF?pMOmPnz5mzb3}7EKntmoFv%^y#|7o15M1GNnoTNv?c`+t0US zFa~;|kc-(pwrLSIYFSX2oUJm#Zg)(A2r{hMVlsr zi~Af`rpI1&;VE30N^L`#VQe7 zB&A|FH}FGi^F8e@p!* zc;r;zU5u_Y=btX2ZVhc7c{3;@wpcAJ~lLU04_`@X=teFd7tkxHI?!WNnIM5<@*odM6$GB>uV z)g?*sA7+3dBCI^-q9Ng7n@)Hdt8maYO?}rLFH?Inuw-priSU+{zBYN1Vj2K8XYs2& zBQC(Rl>t-Y-oChsuEk~nR(-=bsM6g0V%2r{389I46hO@J-TJ0YM&h7@+$)wZDgWUC z46o2!nqyp%4(ukr#vW#cIF16#mDBQ=y9Tx4?yH4+5_mEE>d>MD;CZc^ge#9dUo#vj z4ZtyLi^XAaSQ&g0LUZBMQ|)!P)Ff4strYDAu@`Tc(9yj|03ja9lRQXkI*Uh4;(3V| z2mtfMw_aZqQzoE`NEUjx-vKava5;`&`yGKOz*&`&HvpEA?e%V!2xJ>-LUy_2X$`BcY{2-^}`MVq56Vg}jA342T<(|k`Yxo!jZ2aqEY_%!v z1<@2atP`NZF~xRn@L+PjtLlbIxB|&DoPs+R$`@j>S{R*wM$G(sCNI7WkvM4sB470Z z2K$|uz0U8cv8skY$O+vLy}MHupxM{Kb>wt%`4HIEK(@rjlKR5*NP|Qk7%e9B>1^a) z*24a4DSsb;ASl79@v9d0WO^;|=lXyDgpwAJ-0eFgYPLlO@MCIx`#%8IjAU(kSIL8Z z1EH$E1s$2>k?auJ4`=}M`Y{p$>XTu-K)DC*04o*zf1lZ{HOT$w2-dr$NcY4BF<(ZT zO4&1jllM6SB=7S$Ohf;nI^YF805D9tDJZpkNPn%IYXzj~gTDd|OOR4~LC@?{q@?2%| z+}bs~_CEg94?^ud>~?r~&+|`^m{T8}8EZ3oG3O~UtK151v2i}^Nn9C4LZZi==h9c{ z|Gnsd#n(dou)*MGN)fR)Q$P4*oj*7%+QW^SX&9bUIH&$|CZ zJc)b1j&W+9t#|o5ZlvNfDgKoQ{FDJYGfCnCjM5;osnx(JjIw9Iov}-*ZA*ctb-dSb78Kg_~zcEhPR%|P@i&WYKWNb`qU zVCy+&8{^7P7z5n99^V4Mu2q+kPo^ANp66k^c=33pj+>f&e|-EIFHx0Qn$S@xUS=uY zSpD@I9@#<{Hu;Z$M3gFeS}D-e@fgiafqWZrW=i8^C}KF7SEfK>5}*-|Bu%XHMz;@I zWt-4ClUpWPewy^O2hH67EIpn5%pBO{w;P2&n$E3=_ru{hgI{W^v zqxXU4k^xT^uCrNJ1c8`Im{s%e%R9eukwYhEUUxLlnpx^FCQPX}9CALP>;0=R>Cyf% z*J4wd1NM|HNszRF&7V@$d3H&W_0}_c+4L(os+;;BtwkCfu2}hlnh83CZ`ux{`ktu~ zOv!bH$};%QG0v3QN}!o8!oo?qWsY|bBXC? z!^tjvPQ2vYcoaetu%$z(7~3-6H6tjxxVP^rNviQV=cz$|yVs&QFP>P7)LOvi0u9k+ zpi%@OREj?|qjZGzqDwwQJ!(JSIoml(W&9{BVjIOCNjX{nHdu&W0Npd?#f9k%NNEKK zLgkmHjQq}Fj{4?8g=scY`(eQ2{hO@W)wmZ1|C5bg;AB4}>|hh$QQ0VbDHOzY=T~J% zM1^2OK4TJFU320@1CyC-?68kJOia06K|b-c4KD5@kOQ3$2>_s%pou^24p=h&KV>dr zSw16M3H9!LxrpEQ^Oi2TiSw30Rclqrsd+rH{@jJ*-9mASpS*u_fNt_{arJafNeXhU1UGWA zt=CW=Dzb4iE2OinG%zqS`IB_ugU9hRb^dLyFAZ?iq*1E7+LeRUcM?>pM?>&{e9?R* zn;E(MjkkAsj*IQmS2}brhx$C&9Wv(Va3iTaZl^?2VyWdGH*{Sf809{t(xvKxD(9O!BO$W zyV>=oJfIpZ=R?re*YuxKkm#VgCV#q*#;ymv99EgiEkge1Smw>rhWpeyFP*a^cXt0b ztl?6hP6|@4nD&Eg$NV(Bxkxv1ss=~>G!&QKTtKm1>i?J}a6|nHl5>44TKVIjCZzVd zF8bR|ue0v^Hu>vtgv~dU1bomeHekCDZ4`>QP(vget(Qp9jK&&lUUtaPC+rKv^S zFcZA~|CI2`{$P;Bq_LIYCgKrD`TgQz_;T zIAy+`xKl&2Ojb=TGAR{#2e#ay|2Xtx?|>=Ho;Xv|IMTejWoV;F z>Ez&GV%a~9vxa(iG!N7Qh1{|R^wC2MBQ5RN-b&vEp1tilGIh?!O#3X16HgDCC9Ica zHIe{w1u#47%EbA4cpI|?VwTU}wD}J#cOtfs#OH|n(qiDJ0slhP|5$=??x>Xm-a9Bv zirjFTOc#d3y>xsU4i-q}7WwXdgfzHH*1E$V9mSP?;d4oZZvK<%efxItz!BfN%-`+> z(+5>8mA&G;94pg;dF$WlTQy^TqW0(Wx>aCsQO~(_wR2`mL~<36m6t)>NxuE4>JTb5#-+~K(WVs zeJ};RfMvvT*KKZVOS7g3cvR4GW-7@_tE*vV9>0uf;EUNwiC7#DL2&T10#}M4Df?$L zJ&KZd@>Fgm;f{(@iAR3&Z1MeX#?Skw&-SQvyJo46XSe~Zvr{g#6OiXhFiwb9tu;kQ z;db`dFz7$o*Y~9Lh#5zpAxgYz-zuiya=x#d?r2*Z}E!H zZMdBf{zs!DO@@H=F&0s9t9-sL)4x0EERqG8#RbS4!S zo=lY?wfk03tR@`(^Nub0-0Bn<@ita;9iy;odIOoR!)T>_`-JYNN`u}L&83{|(V54k z%;?~~=4)R6cJ!OWSCBjL{f;wc>^bmxK(2Owv|2|8kvsp)7`dZ;*?b=44RIRf^RDrm zJd=YuP;|&B3t>}u9sE-jzaFg`x4{jqxvef;4*Sl|qP~&T#G!C{xCq$T_*SCT{&Vw) z?*QXYhVGl!y&vDJozAWB9N*EDNK6Q}@SZ#0r(bUKHDggfDvxJnmIn=*toYp|b+Q1Q4!jM^*Ts*6ZZ2wZU0{*>%%fzTCl7TBS8Boz`5mFCp7O3OR@ z*lJE++w$N?-OQ!s4-cBLl7G12W^;~7tj{w@O06%G@@E1Q4JjZ^WU!(cz=bqlyL&Z# zO?{Bq2`#1%*3ImV?({`ab{^v8vxESWBNcD85XLFw`861|z`LHWExn$v8<)vI1DV}w zn#(%bm{t&OAO4{4Pr@YrA32cm{TJ~X^9xOgoRjEk)!Td1FPBHXjgE5nx?)ekiY7Tn zRI(m@EiMC+KlM)ookifeB^si#*-C26grM4zx%uXmj*c$Y{qc>1?^jpxhvCKq<4yk} zDcom3GXCappLQFjY~J+N-IKxl@K@@ASH)~4JnRE!C#`MHs>-9LPeZ z7LkcV#BbXeXfVE`R1k=XC0*@H)avy^bUJ6+zP54leVq*Krfj?)B*&5eAZJ4}6-$j@ zUrw04l@*NhiCVWbc{?YxP7YF1LZDI422swcGj7|ue{}($nDj6X&c!b8*)@Z1s+9&m z@dg4U{+e5I1z|A`)8BTpg=PH)s~Tj(51w5xQH!{ETj@VUyCXUR^;=6bTlEUK(9108 z7&E{2xQ`>l^iOAe69t$7S6Tc{5w>|_*%_L>qDrGE+kc+d?6ZQ7g_`em4S=iLxM^Nx4GpAQ8a(!qGB~vRRIYDI zs`$mK#?vTpn-6&A(=HP!I0oiA<;Q|**xFc?WR7dQ>Smp3-!3evCecdgA8dACtPMd) zTO#7f9XoJ4up~0TH|A9qvnf7k0RjK0c5H7?A@wgLA6xlE?pX)?xXdO3+?&3KP0PU2 zlpkYsbQ_6fOyA0hAoJUc;~i5tD+UD-g2B(&JFdTJnZW%8#B?6eeXdb!YTyqoh~4~q ze-*C8%E~&p98z}lmRC-izvjFRqC?W$%0PFKTkraQ3Sii7FRUNMxgVY2EcULY>&BNk z_`2K#ZX@c^v4$V?J6EWvrG+6Iri3hxvg4q}`z~*YA*2CKlSf;dciggeK2=gu+Ihg} z+b`z=iPEAIvfc1tPSM8ITZ$i_eiJCqT)}f>F9HKcqH3c$3+u2B0}f-nuHg!ido^(S z!>5~K9e+mo*nhPrypXb;tY}&ar8vXhT2>tI!-<_#$r*YO>vNxpsW-n9>ho8J9qX&r z>Te&<*P}uTGSz(D*Htp+^6K-ODm9<*!*m=UAK9bW))6Ds8`<+OC0q5b-S3y{D>tY=5Q>4dgF(xmn zNbf>22;ET7vvT-HhV&6fFKlm0Os++oeBq}G>aUGMs6rNLJTxLXnQDKjjb^@z)+|vK z@V&~2J{xZSnm=uo;uzbe%?Ms}G~4zUVI7k6n{(6e1kwQ_&8@E4_)19YNMGegRJq=B z44))@*Q-a&Fuc=?Ko1%PG_b|*vokh322+ZdFjLWRml@{m zd~K--a2ym0y^+Gy4GFemKt!_J&S_MTd(4W_2`-|JYb=MC6dkoeG+k%oi8$H#eS|$z zWX|ViaK45C1{^`K#N>G85UfAA-_l~v?-Tq!kd!FH3!nFQHqkjBGQta>a;U|$1>aa7=47XBqa znp}-_m&A1}uJ-4tKMT=9F!C7RirZW`Y@3@xe*%zJZSRc4!uZz7tk8$@S55I=6Hots zSoG+mTYF*BkD3B&_@>=|zEdCQapu2&b92Mk%-8ZIAAP!oU6&a|O;m1CmJ2OxRV%vv zwQULW9t-vq(3)E-Kc@}&$c^@2%T7v2fbB}5S;{uXtp2cjzz>Wfdu=j&9zd(}1ar0r zH}aRfDagq`*VG7M{9JQlp>{=&&~1NCVB(mq#>maRmrH#%Z4$&Y#T5ivtAPq*oX7Qa zx=1Ps(vH2<1~24|-g>qxjE53}%BOVpotKqlg=Qs~ANUy;>$9Wd*vIXp!GJ4!y3R^m zh+Zg;?i0b%cl*&na_ZD9twenmPM(iO3Cg(c5Z;rPKe8%Nn=I{Y-T53(NgPwSrz*&C z7=xAhd3@u1#+!nIB9Yf#)Kf4?cblQ!#hu3H@Xb?9Ku5>`m?%goqG}+AtJZbW+QX_t z#k&k)u(VmU#{}@6MpqgqufZ}=R3V7@eUkm?p8x#38RyK)+Q9wia(A+{ztF_jf|R3A z=|sD`>k=*aj(Vd#y!AMKP9iH?`N9(=N4`bMDe$&-)3>@LU|UU67m$p%Ky-)T3SQ*P z{!8XhL`433@fa6Xi)l4rejFr=u9v}};1C%ost-|Rs>^mClGaWxQw}Rq`nbQe4%<89 zL5Eosm9`Rg@S3`srMvDXt?P!@*kNFcaejqK;Vs?U5f}55A@9=ojN27Y&u`Cz-#?^v zW0gsSt`Fsn*w;ca=p}zCB?89@P2+3ef*R2!d%Ui;dxYC*m2?;n)89w`mSa+6V;SnW5ewSYvQm~viJ~IjK5xA$ zc}fVDNL&;ek`oh4rGY$)gibJ<`Xml;c*9CKLuD-Lww};JF8ggo)?V7{#NRoXcF4HL z-?uwdo_%vHH~~MW)u_m$T(e)OVXb)+T(%kxcCaT?dV9Wp69@I_YGv5l)n#U4`dKt= z-!LKioJ23YW-_Y=wSt=R-X|(s3++C}VT8F|YPLe!-B=-VvV3-ViKCg2mXJIdwdTGy z(Jk}E+}e%`eTGD&=V@l}2Z!=vi=rit*BpF#Kb<#jgpN9*QhW~EQTYB7*AcZ&>O)Et zH#BQ6IFPcApVxH(kdQ^z8E_!=N$YXE;3n`Ah2`**sP~(^TNRUY_>b4SQ@UGXGLKD?m^;W{B}`4*gb|MDr|Qq ze0N4wGU?^w+ESF;ui%=iuEBbHYG==s6(Gj+L56`-@{uvvR*CB{mnm_UIuo4bfd0kf z9*R(DB)id#CIFm4<5&b#CKndRIG&v)gyt=W&G-YZ{c_#%aB*$${$zGeNuI~!pZigh zjshmN`)~X^yl5}}1aiLFJB+y=o4y!QK&x6=Jy3*wz0~ZYq*VYYn3pJHXBiPYz0}6O z53JW_dfpBnkfYkR#Kcj@V~IKri>T_Z>WH@JOwz^y-a!zZ9Q*H9YQquvzT`bJ`2-=bbFnTvBF%9Rm=;i zTzG;dQ!dXBS#AZ^25O9jejXdAbD3Y9S0&S_K(+4RwaL0JM*a!G%CcZwJfG1pCLI<) zHuT_L3{V;i{IIjS_q7MG)WZ3Je_Vz-ja@hjolk>0*6aBO^5vkpeh{*q9bCr=+BLzi znTPwsAJ${Ay0_&QM%>o|@weNpFjiiDa_Qks>y}T$ z%|!m_(W5{IXLB><0NW>*?=zm%{Am|0hxsOZx?pWh;`FjvOJ!&rax7302eCX2u>SKt zuyt36qP$gZb%9lFdg~GwE#?-v>$nq6&kX$VMV0BR%UiW9#__V*jUQCZ{0qrh1th1aWU_e6Qh2K$3o+Ear+iM6opEAQZqXB zi&>|P|HQ`;hveL+3$tD^E-XO8rL}O0>@l~n;aM|#ima^T3D1-9|1k>FFZ3aH1qNH ze!mm8o~Go{KzA9QQ6o=??3Sk7gF6v3^rIVQI@&h{aP5V?GHyjryuUnYhv=k?e0%Sa zB~|`Mu`i2C`4PVRL%c3a_s;Kb=Q)Jm91I#=gR9e8;WGW>xkSbKFmi7fxKb_{(4H&@ zXFkr8c5)z@6ema*$T&RQeW2RGmOb`ly-ARbw^xhum7Y}&R+j6$rL1H8V&rY{%4T(>&<}MLe?u|vT#Y#)lWF#5fuJie|t}m*4fK%(|1=jp8bd88{#|P6C_WJ zL@h3anKtB+d&jjBzJ*r~c0f^i8h&(Mx&A8fz*7$Ck0a`PKi<2ryWQgtPzvV;bX}C& zcf-I96+Gegke)7F$O7W&p7x>>o=Jv8jUBG0#ajtm!7jJPPa49*sg(QJOsWn5<0~^A z-Q8T`{kg{#JZ3lH*sybWI4PbSr^;EbTMZxDIDs~;bXM#1%P%W?*F;AEU?(B5t`4^e zuP0DA>YTO~F_;pBk7-Gx>!#Ve`qex?n|3$i<_8J$ooLvezDY?Z$`qo<1*noyMP*mK zB&qcTGms0dCaY~;X&HMAew`5G@52@|YATB0_MPvTlik8TnM1LmE!^V__R!VU?E41? zx}KO)E-DDutl`jrqy%;hGkZ$qvSqn*3&a_>Fo`^#rYg=#%ksas)NYRNjSa4^A=T9& zBOc!gS9^O7Pn2AKlJx2He}0fA&Itgfck@Z3G6CQW*We%B*jH^83+^j*$g}HXv&+Gr zQ_D2}>LrmaQtPAHPg)t{cCf96J9--Rgg{_$sM=35oJ>gusUy&89QV&FA^w#})0uC= z{LgUue)}Fp3g!sPyqKJ{FUY&Sy|s99ADm%!0tOFY;FZAMD|vh9CaQ};)z#QX#ScIr zQgWwScc*UDqkLo>ww|oq&YT%2@ax%=wK02Sx0%6lxFs;e!RFCQEqt z%|T)SjTho}0+Z3v@%-I#7&}nw1r&R$J^#o-g{A}jK49%n=nBc?s$6)vx~JI2EphS9Va9>Lu2=GS!)P6*c)BJmT&<;S z5w$hrLNxA!LP6Od5Nkwkwr&*5cdlnKAd4W|AQoA?{HeNc4g{`u(^wf9z2;B{`i-KF zu~U)~nv7lZ zlp}7l!<4CW7#O}SR`q+&a6!Dt+Ti-vrsWp_?{_BJxlrCLt9M3GTR?SWkIUFOKS1dx znNqp|`IYgifEv!EEJ>9Gk3y)!fdsQ~4U4p|?jUd;N!pO=U;SOJSh4F!>v5J~&JcKa zd#Drb!T8K6W(K>tZhD@2+Y+MpL&rYVr_4G4r!OgXv?>chFqvB4+uiXjq}k!hl0OrL zKfanDkSG(dZWUfMw$*tw;qBBgpWBMF{?%!M*4xt=Al9PA`ot}I*#RkNJw*P=iLCaw ziuD-%wv)~WD&SnnpydNqXP{<@CP3i2^NdCVRaOqA+)Ro+DbfcM^vxUVw!buQ`7K zhxeEJ0f!*ohp)S#{DT7Cmew1p*0X$wqD1UaIE_gi^N+fQi-Hf5s?)izYejeG71OaF zn4C>m#gti}wnoVB^7-#a6<%&RTC285o%T)zLE*8`UQYB9wJ{vyG;U@+&A#dh8Lrur zd|!|;`<=1}%V%*(=~EE@vJ$au;l;EA!LO%JbVP;3R=#t9*O~6;={NFdO=LUy`Bv3; z)G!Ea$w*T z|E>9I8(CMQtGH#>eZV&Q%Q3^LArNQz9}!Bm;7ozGQHa@Y0(3h`fV``r8Gp+thCH=$QbrZ#>V8!wNa=kapz|9NzU)bgO}Q?rj^aF z#w?p%RRK6ZuAt*wvKf6H9L2>qF#*PS?h9_-5aUX_jMmYQ2hS zYf0e~b5h?%GF#8o38~VA8T@7ZgMA5?55AW(_#XMr(-N>kg(n4s#5`kt^vVW0hc0GyG9L$Ow?1dNVQXE;wHr#O&RDSJ# z#IhrCZNJjS#RJ;+AL^-!WNC*ucj+__(47Yuo3DS{y%zKa*u(xAdkL_>Bc5U0`KdK@ zyy1fP64|NKgZE$O$=SmFl`;-IbQ$yaegV~L?@wdoh~TUX`%y6+(l*;JGBW*Y-Ln7) zG8@S6DrTRES>3Y?q-)(VMikuQy3PLzQegfzJ)iwd`rcUEx$_V5k?r2&=)7lT%6Yv& z?Ua_FeZO7ywy?19rRk}FKv4r=*;I`yi&`@Zy07V4R~1)}xleKs{h!&i50l(Ir`6RX z<<>`&B<&wSZH#1zz92*$J_7=HD<9DAmT!K2T_} z`cW%<<*((hK^Yk8nUgv8uCg{WTx+SXZPW)S`4P&9hmq&Mf5iHl^8A~2CSaYc$cdFs zfinXi)sX5y!>Q5|m5|A)Acuf0)73Hs9CZ7AuUSS#F?N{Ty%sQ(BX#T}MWt6qp%OA# z>+_>C_M_acG>l!6}G@0`!IMCaD39SjP${U;vXaG-NL^rV11{{p zcbJ`)7pI8VEFemA^87Hic<#gBe{|ft821e4gMlPGy_*Zw9}*Gpvog2W3^!yoE)^Qz+u9ft6Hb$-idr zMsCp@uNPjbN8-7cj+W{mHjP2uo!W{g9?0d{eC2wcUxZNQIB0T{zhx+K2jnKgi3~bh z!yr1y9bJ)(ruQdfmj;8^r6k>On>{lMbiNM|aO!R~v5pgzY~TDwun+fA&a`L5l1J==+UTuDoQB25mpC+Ll!s^YF@vg9Z(E>x zCAm|wiwNC>WUAF=X!zmHv*unN>Ns4p6q{n|C%YH)5|r_gZ&+4STlcDz)d2n3|HSgw zWDN+vtOrTwzm@b3CKC$#aMilY)OzG?yaH;(0S_$J<@b5x=e~2$6gz*{sM0FQ)AcS- zjoh+(Quc{VrBn=XwK=!AL5EkDQLet=p%Lza_l81(%EMDs&#GoP(A*mWErDVBOjGi z*dr`vtRg+0ratr}7h8{!lMqpC$8SRsqvGrMm2&L~soIbG_c?j^lR>s*&!7ZT4fFxG zfok7v>r7Dli4XQdenxGv*<577`rBVUcV5oBljGgrRahbCJ{n(qiWXgW+hIj;`2FKj zLSIm};OB`XAnwEO`nJ_cisauBgfD&Z>bP1F^92y|ACa!JiG9d3bkFl+0k?r}X^Rbr zyPLqI4V#g%sv{mVcEfn@-pGwS6Ha?seC@X4Y5BFTw>MNlwqZ+adw!sPIb=KU5(b_T z+i539{0DR0L8Dde#B$*^OvIYwZ}v+O1}l|ihraV^RQ`}16MQAAqXcrQbHKX;{WT92 z^R-9cdiylALT5oL$W5mqR+1&3!=DyGQ|Oq*&OIieZJ|J28J4whiY6bgbe)(Aa#?~r zILbTJIP`C6jrT#m!GMu|jV&#YsEOt=YUwzIF3b%)N5$DVULMg0J;@U_DG7DG$90dV zUNe9-$_AcS2*0t55ZLD-;%*U>xoR}vxfBfxx(TNMB`T2bF9;wX=ZpiD<5|?rW9_PV zZu?0%+`z(SF35mJ>9j;dmUQWsVHLiDV^rnaRTGJl^B+b^o0(-(k>4MF4@!49Vb@>P z=+x%D4rQH_2SeAITa>Xgrv+@*of`wBtu}IN!@CWp<3Avc$%mvD3FHC6MIsWkozLCG_%059)z-jI zG%YD1(JSn0FUln*(EXXE5Rn3k12d1ly7%U3;FWCW7g3A>t$&FrjU#wV z<~5Lpn8@3RhnKnp!r!nbtHU5NKzC}p+lsC%;B76xFj8W9dS=}0j}$i)fH^}3HrRin zfo7WqLIZa6Z+_!7dvFG^O>%7HDTOR$z{*vw_SV{(jqzuLqk`uUTQEOY-uf$BFuUML zoB(VpsDr1QS>Rs5wiPHO1oo=bM`6JZKzPe#3Oo5iBb>X7Rpo|b%hI>hE^5ONuW8GX zDowqgXb9-Fl0tJxb1I_O`I6PU7GD*3nS|tPVjsROuY(UNBaY&iLLcCeiz~DF}S@{*se%IhcGNjR@*HuUL!&6}uI7}AoQnv6EZyBF` z{l?kUC?A?QRAQU=E`fvQ%?Du1`%IZ8k}p|9a4)u6!c-DJ2%)3m{H~aNq>Kz!<5nWD zjaDZ)Q$~Igkv_!!HqQfau;Ze+#r}0;Exhk|`pY9l%~;|goio43KU*(a3P4NH2eocl zK0zsT0SZeB06`z)YUgZm%$3^@&>NRYHEri@I%CFB5FD$&=?3Z=aV=)&JhYH@U*e!B z9iFnNl6wJX-G68f%B3gVByzX~!}j6meW2&dWkN`3k9-I~vP@ssKZt*;)gK}i!e6YP zd2RmT1LFD;0bM4mvXpC&bboQevy@B&`Oc9yad?`qF7=d*w=QARnR{?N6u;}1%-T;c zuwO2;qwWqrjoC7_biDC1MG@N<7Ozx+Pt-ap_2A80}t ziG{aErM>~W92_9?vy|s1+P`vJS~R9& zpw~AI{mA0(m3ax{G3~84dR^KxUvKi-xuA=Siznti?RYM74Fc<6?YxiC%T(j)VAWzaJ8KBI+PDG9ZbWqG(Y z%8LDJaUY-p0Tl`QL>|UdeQa%{GiZb>`Y@yd3Q<$rKw~)W!Y+ zr`%HLnXC1o<@Oai`pG6Wgb8SSC@)zxr{@PcO8>s}5=qAU7e%NNK{@z8%e!aK8(bUD;d#wB+0SgPd#JXng>cw_lU&m zCa*Hx=CQALxb@%qJAY|y)pHKLvtIhUJ)!mQIrp=<+%4eJoN;{26O#!L<3hf#3C8Q6 zn>*=@3M)A(E_B$*na>h{Zl1qfGV^S}ci16|QJdO>Z7LGT(e_;i@|xcm6SsKgX-KpY04FiiIDQ?@y2X> z6S}M44Z_Ou@tZw`660$I%z}ZOweIB-)W#OVJ6yrMG*&O3-lPcT1Z0jd=6Im~W%*lhUWWG(JZYnfAR6~Hl z%i2-dWiLA5v^%&aq4{tFOk#W1P&YSkZVnulsMMxCtKjve*`0>fZ;aI)xK{{TBJ-Lq z63AB@XTqZ5!`v5hhgGDQ&*Owm1KDXjKkmGHr{BL}XIy3Hyd0RA(e)U*giXqtCldO|!N%nm~rmoc;zeQk-Ivp}UPd^?dyma!3nt!TdoT zVuix(_`Pec-^!c;Jx#-rxKI&TXRSw&Qc)hu%BGY&rn#jTI3Dr4vv$0OJG=+$hWgg# zmG<9PAPTkKMUQf4y^amq5Mk_?hWyT@>w_;U(KU*Y&s|tRv6IK&8OQ~PS-j6f%)&^ zYqsijBy#-F=GxSI-n1Jbj^M(%d@1JX9f)T=ow__xQ2X0dR(9Yqr$RqcUM4Ol=Q zbC}M7{i*XtC4j|!j0^93=Am5IE{-ia%;G(Dak#s1TK7Dx2i^rS91dJ%ZZC_sF}TCt zo|dQ5tYy#L5f+EO@+-PvZ~HP_CNL-MuKyg#bbuWw=A!(rH*v z3@&-$dJNeA{dq)|6G-9aY0}7Wr-aT-N}*{B)YF*~!}>uQEtmCCBR)H<+})cNLNpe7 zx_H+)(|ua3i<_V5?1JP~m>(c2yL4yKecGvLLy*gV;sR#JzFIh++vha`1SP44p}A9M zV4!j*>b0`Z_i4oa6_D&{)uZCV7sDx-S2|N><|@OFlI!-yD~)P>(FkNW(UK3vpzltT zdaeCO7QB37s{_}~9#^B+k6*O;ORm)t`Wt8LXBi1fFwsp5HXjJ&`CHtX=4^nyoG0j} zi>D1G7vtX;N|ehPM1P^sopsZ39beqNE53Yqb66#LD21sBB5VD7JH2jsYA;>+Y!^^( zTzmPK;gp|TvWFcTRQirlGXp?x#eA>F9i`s6-*Mp-?%cXK!bO9aR>S%G=JC-5&8*)s zO&~d7cBdRroweq$(%bKfqCl_S=b(WW?kyKZ?#?qfg z*tFb&&)S9Wv4^MS%DIssL(TccWF}T_Q2l-)`VeEJ+6gy$Za@Rn1R4YcTl}8nS=GUo zTLuIIb^C962s*jZ6Rx8Vfo8++-yEU0O?>nTiOR}VWqLHn{+>{S`Qu&R<5J;}#d9NF zhQD)m4teKmYA4_z0UFp6TFxHClJ_Pt6{<(y3IRfk&Xc86M8;o+i){rUL*~@^mD!;d zUKU!D2Wbg^AdyN)vsPsB^qBr(Cmri1EA1B<1uimA4o z-p4k^>#Hs9o(3VNO1d)hMTsB%>oLn2hwHB5aR!*D%;}{HujiHB<6U{Y4mFqdlIi%u zJ>O>u)EIuTAq8x{h-R%81nOK4-M_k&k*_k9@A1}xAGrUmS;lq65~SR4XKDWUY)SQ%`+e>Ha7h;Yjsy2^4=maO z`h-W>DQWV4v?-k?6Hb9&eCHCX6GZ>i{%+?Sa`P6*GzdK4!TjDB=Tcd_-#{iYY(81ejumxswp# z5WI3qyuE&7?+LTQONN#2gv@UUy?Tiden^=Q&J}51+xf!EKpkaQ_vL3>6?faZ>&M!i zwSf((j9s_**7n7%pP(8`s6equ>_cesNzNxIo4%3u4<$CLU-wYDE1u%3m0s5Y^T*FC z6pdXN4n4S`-{4_ttZ8RpabaUK;SjBRz(_l{=%r>-xLhq4R*9{Wz&WqVRc zSwgZ?5=kg3WY3mHmdG|^m~2TQvTteOkusKK8)FdC#3;!=#+KbImcdxwqv!X>@BO@= z*B^|T`=0wg=RW7Uu5(@A@Avd{Bn>dvayrT0ko6T8N5=3n62fHyW^W}Yp-NyW8qbu? zmi$h=`At~rX(kbP90QI9YztYx{5SuxvwrA<8&hBvDfQLMBHIfC_5Vckw8~R%lS+J6 zVO07w|Jkj$-sY9(lUs>RVHr0jMi|jc1=d}=ASMc>#PN9k;x|7jgTGV^Lv^Kn+S|i+ zWI;q6wJ`O%qm%c7fmgWMm$-tfZBh+>Rb!sp_SR;5X&J1Rs!Gnr>5s%X##N*NtFc-q z9WX+Vc5yysD6TSG@oCQe6B%$A-K(Z#efkyTb!_})eG=lM&r~oYsOlh$LTLpWAyGMB z$tEBU{pJ0diTbMmhN-VRhYR`l_isvhUxO3u^z3rX?a45Wd36axZ4o{#^W!}}#QHjh z&kY?eR0`%YgZfyh*pCH2+BXi3X7%65_u_idlwa zn5hXg{4{L3aoAY4d$e);9x#TA%sgYbK?)F!HHpQ;5BUk91kUY)PhJ2aw+ zrYv8?@;}oUbwjRd|J+_f+SeG5jq1L|nnBGGRmW&LF#U6JW$%(~l_ORi?USN5#r}b2 zI((sy!%XS(@xV*pqe-p9{Bp4q2g4dUff4!sgM;9xf`)$KC>F#ILFcx`%Zw zeHtTPnE}*pemr>sH%(Yh@Uzr?e)-p?=2O_LF=5tH=-gL9JGrxzwokIea z$)pi+q4e@i%Loy2W*vZVEQe)ge=Nk7dvuAZN2gDmpn2W9w{HC4{kiksJbDYXi!621 z8%rLWIi*f?co(u@Xi?ZTyshaNRQ2{ZW=K9eK(4MhhRmutYFOX|w%bG^@3Dt%9VJ$E zA>RIAcM_x-{2GL` zRzG6PO0P7p2kW#LNycH+lnn&=kt>UgggrlS8~GvCv;W?3nI}wBesE-*u2TL=3+p-d zH#c41uWOkHD6tCVL*?8nZI1|ZE~$9KRU!FpB#ntp`lJ!|+72hCFT^Ag&7rmI@~&X{ zL0y)7qr2z@Sl(E>-FL#VoV2&*|J`hfg|*9mE_54xlv9tR{wvd0j!3sHLe3?4B}BVO(+F8K^iLc8`p??;mvIS+@e;S#TdvQ$ILj498E~W5BthF5=_Hk<4#6Gj z>#IYZdC~2>>G@BV{6(Aiwv7*ZKc7GPunvaLc>7#epXKo);a22!65^JN(D=~(ct)!@ zT27m-PJ_2%an!2z_E?ZKxiFN8qj*wn@@<`g-HJ7BFy zw#?6^#~o0f)773f?i!+!+*FID#TW`BZyyVS&qA#iYwB!2k3WBQ$#C!!=3&UO2O3yPzN>eI&X&zli(d$dIcxZ_cZS?~k z$v~8*U3rA2OEdTGY7(>Espey+CuQDg`wv)#?8Av~)8CGakjVye&nmoeXQO|gKe`bS zOgezGZ~YEM-YRF*;B4C8;b9eb0XM6To+YI z_*%~Yx$LbVrh#SW@=pQYbFgbEqx@l{nzkvVqYQU!obABiDB#ll{OvZ^l1M*Mvjek}JzKaE9sC)4@N4d%BS~LAnwlZ_VF`oBlt=QKm z&!QkvO_$oX6vLbRMJ9&+S@E!nux)*2$Q&GM9w;$O81H^$@x1lDc8izNVe%nKc-waD z7Ukc>$SvN!Rsn0)EGH#aHqU5;XU5HT-8o-SKWW^iIKnS6oqceO>^$1xPf$!oH;Xr|;8QhV0IwI7Pjuw^LPJr^)I9=tZ)5GI5$ zoH^13Vooe#O;!)!0UlS3HQj-wn0P)WhBA!Q0JNvmM$?Z`mNS!tg=({LoiN_UcTVNq z>=}c!maL@W;|i(${Y-H3wZOI0ZqNJ>!6$Mz&lA6dUg2=)XWSf>W>1*W0jtN;L73v> zqN|Ys*JJlg4aJ%vyNbYMPhR&sM%6McwKA3}J~)w}nu zeD43~$N^ss!sex<)_T^?mJ`O%O$EM+avlEF_6@w0`dvq%dJZGEyjm!-sEBLq;6lKc z=KEddOu)clY$o=AD|ja~FK@sLCr==glXt7jH{_`Ak6O6DG1hH!%i;FPb7X>(EHcsSkfq=;gy?(La-%~G0B({nM;h}mnBX5qykB%QvQPW~~ zM6i&%pq)`7=siRjwC%xgRXCJ!niuq2FHv8Ks?x%}yF7<&d~|3Dzt4w!PK`5a4WS}B z&mr>;xOW!^t4pIkguWFfaZTK?Zeaw#7oWkWgXPOH<^18Y>!Wo^0*)e^p zBDlLsFccNbly8^-n_BjPrMTXxx-k7yg$(=I*> z5PpZ;F`3yV>mdgl9Wv{2v>vNCUr}Cpq)^yrImMMbWqbt}HZCJ*QubLBjY3-^w#Y&0 zsSh}M?b~t}`}OSgkcrjBm5NcGC8$bvYk1w5J8##0fO!%iPiKe+UAQf_dEdz(k7a-R zNF`K+Jb;3jHV@lpv58irSeJe?fvTLj{B$Brdx8z)5X)%Tpe=Kln8khL(K$Boc8aOG z#OKZL*&-*!+3SV}+8`=1o&gmgxUJ95PBfQFnu%H*t??WfGv%Ks@jd{O{|z?atkJl< zXeG)yzFR$;2CsW0PA)z;^1_?yG%G6c^7NxDC*EhzuH7PPFG#{(RXxm@S0I68;QiI= z^6>AC6?4XbX69qo7^xeuDIdLg-V9^&Xjy?eJ3X0zT#Gv1%p}-YF&HQyW^x>?Mgy>A z|9+)yGTQF(4xw4VAUWAisFrx)9l!T0e8H1QH+;E4yE`TIaufATFV&SpZ`uIZ@zY)3 zw203low(aC{8olzLRA9jE7;E;)8L`xiZ`FJ<%ISaeYxZ>m8l@pCTTb+aNRz7&Dwj)k2kFYrIiW58YJlbYL zNVWVM5xMKu6MIR$=0^lIb6g68cCGfbSFSpX3su<0AkK3sI&s2jO<2QKmXPI_8qVcW*GXBM<6H0BV-xl$j9d~q;&om`u5Giq76y8MbJr?3jB z&DCtTn?a$VYwCiPw%8T*cFd>JF-Bo!=3aeifah`{sivbok1C@^xmnyfmQ;Qes|4F!D|g0)6r2NU}ppe7#qP%gPcGA%#J4}@fU;I zTzN!Y4PX7#Rad+eF?|lA-gtS2;VJxhOicAEqW$?BR9R^CpBQ~)Mo|CLTtM|MZL_^I zIqcinFCYTY2b^)P_O~tP*;u2Y6)c;}8O_uhFCDQ>!Eid|Gf2EJAOc&5I($#du3jv{ zWTCSsXPNEs;8 z7ZVLOnX3ESefQ?1KHYzW2=7(;2GBc444ankJkf)UMFD~-JA4I-J2*FU3Utk#bw{=^TX=iV9It>;t>42(qMLZ}$At$97UFfF+=8I+g zRx@iJI>kgikb+5U{jhrLbwTwaEv=qyP1{z$Ps~mcr@>GfMw0(rK zIwknH_?I{~-HO3*pM(6p=E8)stLrPwd%{Nt70~2xSr_A^M~USxUD+tws9+XTBQ`58 z7ls=--!@+3Haja_n?s^rV+WzN^{=j9!Ny+bJ$!vKLYArzjpmAC z$rdKBrsr#f8zzDA64k<0Kf~{xb;0dkq1hunZ~l|WIghGO&|4)PXB&MI9fRYhR-YCvuzEU)e`LTo42UL%XD+<^l%b37b~$BeBZC@4 z7a1b}YVNfQgO&a)-0DFh*Zma0TC%1w`Ul>&`{_GU?KrEq!UuQ$@VEeNl7uet0XP^2 z$Oro~dp|TFB)Ip!?ljvAX(?8}Cdv8*t2gF9goRTnn>9!ByFIIx@<$7%cMcjTol{^f zSoGok3;v1>>ARlGGa9#U)*HxEq0vbQo_jh0Q@X}|16@l{DuAGEGs9?ql3ALHuT zd>-281i3u}`AtSXr;rT=!R>)^+3ycTK_Of>su!0S3LWhJ+#+-)pY&WPcW$N{L(m4k zF?jfqzSgmL23+++D7D-cmy0f8=xXU1m6@2}a}1RXn44I`m;QPL+GzwoO?DhqSh-_A z{UHt@dxKsJ@g}(C%3bS^_3SnXpPfm8m1|x$NM~4$g4EAgV|gzKj`Ppaye_wf$#E53 zELE-6j`VVDV?hBEZTyU8UZwSlmjkd-ZZ=OT! z<#X`@=v~oxl~{dtyzB{<*xLEMKg7wsn<639_uRm9C8B*8RaDZShEAd&3DT=i8b{<^ z%2_pnr zG1>Jn00}-QYJKlym}I|>h(8`-o!MGK_P>0Q?{Dp#&?ydtPtuePPml%QtNs5Hisad6RQ% z%$wo+&8QUq32Gwz{26@$s^L8F2bb3NLyI>0`x0+3a_a3hvF$dY6Yf+jupgNbN!RG? zYCp^U<*q6>FYh0I(&|22>NQ61BA!8YYv&g(JFm~RE|5ZdEgEQdmq_`{J4;&&6AI*; zTXz)n;*#gA1GrbeH0>h*N)cHiM|>mA5UkX~RZwANF+<;pK7;zs*HeDa$n6S7XE5PG zdKno!apW9CystaKO*CAl1*RD=zHvc@A)n&E%_z-j;XGp@P)|Y-#JjSuR8hXLzL;$rr`QNU)7qmX?6MiPU&z>eunO{}8 z2XMrDK{e2Eke=j0A8_kAh3FgWb6!T^ZWF6<$aoX5#(nbjOmW@8d~Aw)!1dRRNm952 z+WQ8LWc-coBBk=L23P+=A@-n(qj!c_lIG4Pe??6|91#t%ODP7Y4+3-k{hB97{(1sn z!fAfuLtrYzZhdJDa<=!w3t6i{$)y z|4b>8_<1ebV1b>j0tl2U_JG$Iaj8s`%vpEESvor(iBF7_dBOhr$Gz1S$7W~G#+9R(*6xQiAvg!IJ9P>n+=H-*`9LqTsb6%*o=!E@D4}Mo+V14bgOz(dORNQ`({K)n|y2!Itnb^qPb!i z@4f)*U#z6+xpT3;=aYPIzaN)P#{#gx>s5l9?I|iS1-33cXzJDdx5aj-P`TffAyAEF zMSNo=@VbmhlUlZdhiku_4t43NS3h7A0#&193jgDK% z{vJrjRSf*+G({C&;Wo(|d~rtMvJYs>4Ewv@uWE(D4i>ftR&O59*MK6ql$k?FA^Sav zE8(TM&55R0vj`wzaWX8;B1@7yZ7XAV_ACTfUD<5ASzGrRBA=h?!Y-ee@ue{?iF!c4 z+lC=cm*6%x109yPEs-kf@K;xxZl^y1Gtfq&Qy?5-*CbrOcpr$r@fwbC45~PBIx5%~@KxDf92L240rJSCn&zYa6CLQyxv3``yBS6e>BYbbjFDy)3H> zW05m8!-^&*!16F7!jGTTy1xHKr_oo%Q2xyz@o>V%_CAjJkb>`NmO`m5kubUk_-A!< zfbiaBaSDXsLKO18(t=smN^$EeSGr?yeUtiJbFN)iqRG3izxhw=4u#_vU(T>=~K4!pm#XAFLR9eaJO(hJ+`Tcpy3U*AH?Q=3999*FD=W& zBLI+T@>PdB%7y|sM5+RuyQ10of|k3opJyU~e)A8{wfaM*AI))Tyid=?R;ST?g?<}<;)XjofGDW9n&SQYe0<=xT}QF8YiuJsxx_;a-gc~RXp@}w7b0b zpyVhlfK@S@3Lt8$7ZqG}fztk)2wL+G``+DK+n!VhqdU5z(#mY`>Yf!3^L^s%F};qI z$30M|Zq)>L4S*)__KgP5@miK-^xUPIr9S8wkWW*=alc4!OREqXBq|IE8aglCmsAHY z$+0oYY@Scgr)ZzWv&B$AYF6LeqpijV`pZwKr{c^)7v_-bBwfUZ;!$I1sp`rb8@dGa zVW=b`7VCU+%3_{0fn!Jp*Xo&j!#(F-_MH5rOF&d~$lRh7n|{nLoSS$3jRJw$_Ycve z^-pbytJPk<$8)Fr2pjhktZX|CWPm6b7zp9YWC4s$wr=BZ&Pq1YAMeL7uz5E68c4V5 zNplHLQM6cDcUER}o`Qh!rqztwWbN}MRqtL2Md#}Je=#0wlNk*)t{GgAZH6~`%0TVs7wqGh_;ilcOEx@G=h-A_M}zrI3^R~5yqIjNMaDAy z=&#|l#~J;W3OWUJA*@+m)Lr9R`i*(a#2ao-U>zu9SpdDp#Tk;?TlRALvtsR#_o2^e znb^1cyne&3q{A}|gbsp9ny+6bdFBXT>u)nRFKUlMtS7xDZJKKjBcHtPaWBFB32d)k zmXPL3XkM_W-{>@V%~ksoJqqNa-g2%=M*G|usz*rX9($9m>dt!JZzNmO=C!MhBG9!M zd-q)FcJHZRd^WPA^VInY7kNfLxqcgvgDijSV-M)RV*OvzyrsGVyCf6D{pyjgdlFe$ z^1AeW0z;)YDP7Vj-bHtwJoEX}DW2RfxYa#stwzClGJ5@v%~7R%ap#--5?c6V3DrWV zIOlmwt2=qB3yfPfO1(w@%Z-r!FNyMnPvoUw5U$agW<>DaUfugeYv$u4&^6a-v?Q{__t&&Dg-61YiqZ^A zSUfDaPS|rrHSMv&rv!?kS^5<;7R0hvS}xWVvW%i=$S(cbqeIn-*_j;uYLu#!NQuKk zpLr;O^GZ4>A!^bp*ar{yHz+`pW%ya5hzm%VYs9*zM4k~=c4Wu>bhW>8*(kyh_eYyj z9}_gz@T5p~b#KT$InIw~vO}cs(s}o`nN2O7Fl(mpy7v3_{l;?8DU~|M6cl0ude$Ob zLB7!8VwS~V*K|c!DrR^$12LMm9%Fqp-1!on?ni*^+rrEG z_Zb`XVZ%SQ5ZIoTDcRr>PmB3sMZcEWJC0$km&0(XrtRNz)kNoS_ZU6S)HMGU=reH|Z-J85lZyH`eRl%IWeB=}N7$--qeDPR;eokAdNn^nC% z%BIj<;p$;-wH)q0y*V)nfZr&6(ANZJvRoZ${G7I;Rh-CebY~>_t)BYBa5sw&4o$pK z^;Dy=fN7p5#&6Cl(R7X-&MiGZX?^;vf#yJ0+qoJ58e>mruitE3qR`JMe`%~;O6|$a zeQQ2Xq|Fj(Z(>xsAg=R3;)ChOJ$9GfZ=tH$9FzS;`+`O$?2YAZP9NDc8*d%n;s#zR z>m@XIiePFIK#|S*=l(Nd+uA zuGuUXqzZiTl3mq9mUwl!YWt>>N^H}v`XFrq4VN4w;tbg58{<=t=WHZMQ@r5B34EIq zO)nOQppQCwHNz}0xlGb=DP6rmL5?A^yc5krmJOS}axn)6a}xQ%Qx1W+A|MuFWsrOn z5bnM-QrBLy{U++mkPG0|rxmI6l{&-TU3|(L@0Ig#8f=2$)x|*`z=v@feC4zAlGPme zy}Y^G^y$#7q704DqoH=fn1el%`{wuaW5MI3%7?R@PhlM_2z8c1Mwjpxn%t6;smUHP z$@s_xhxzRDeA0VErj_Aq!Vbje`CrRh%|7-A1ZEAH}IV_3EczH1(E8 z0K$5e=k%Ga4Hm=BigDQ4G}Q)Kpq3+xbm?oA|L{}B-@iC1mIpqSuR>f`H;l)_|Jc;8 z_3FCKoAmZTS&T?=NWh*3T0|-#yA_rJnTw;!O`d>72Zq0;!M2BE@DEhrsDxI5j!oEA~ehhDTa!TG4kxI~oZJTnqWdc|_S1zx)LE4q#0SVCAb+q+ym6#jlRLJZ)gWTio9rS@SFAu!fi*^} z%8$`esBZYUFie&_QeRQ{w^1k}#i3f$DI-@XP%|ho_Q#H3oZ+iT5;`fKwv`|cURFiv>JW#_b|lOFz}I$%a7mdL@k1d2M@@H;0fnm6w_ z!YL^7VmZoa(0Kmx%*NQs)QW@AUyt`l()mC%>R}WboTi}nNGxfY9-SK?q%(dA}-k`)NH(5{l%{GJr7}A?p`pQ9CLsa>u(C6D-BNm5l5u#BQKU+&C&?6a950T(#+)~fVcxf8h7ch=s#gympr$L^E15A};h zC4_c|{=r^7u&MOZ z*FYZ8T)8(Uw>dESOkXV+XGER?B8o_8D69TMhuL5B1#~&sH=z4%2d3}19ZR^!%TS}! zC)WfYPWdC}HhB!WqV_Ks1ATLGfHebJ#vrqOxT}BxR{VHGeAwY`1Ahfz|J!*WwXt1% zbVjuJaQlJz|FsptB5#Vv51_Pu{IAXT>Zw zu{$-oGLx6!CRD@*uEyY|%|dV&i8=hXrzPR<`g-^c_`qcu+qj&y*n6lK#2UiY9U!jy z(S8OZ;C=Y_c%XdROKwAkl?m^?fh&U>-jcxI%dzDghhQ&3dS`0DjeP0So5^6Jq{X!4 z;aNcYi>Aet);f>vJ`>&;)qr?W{_4M9`i*JCgnqW=?G!j3(Z9W70l~C+gG)T&Yv?v- zo919M^fsy4S#g6D{`spOpMWm&-b?mAMeYEWE0aMh8h_Ze(p6~iE>m;xg+?}&pR;hF zgCD!ajrYkCk%a4>9Iim(xekvy3vms>??^f{2VZv{VM7gZ5>G>lX4Nl&du4MRsA+YW zT9ss*7k>iyFkjA9^jfHfS@tNyN`FAhN3 zgg|nCe+Q-+l6J%YKXv_!)ez^wn!r+dhr%T(Oh7sISp?{3!t6uT(Y=w1bXTyq>CI5bhf7$q*wx$ zM*y%aGPTi4`A-7FIwc3ZZ|%P0YeCgR9Nii~G`u=|ay?zX7O$~cFIpJuv+=^y?dLnSo|p` zBG`=%LW5kPY#;7Y&lQ=vlqNxFt9WRg^3AO7r_odlBd@|ue@bX8EG>plTb$KhWWF@y{prr_DUhU zf@3N`=Gb_iiOd7ZvYb`1Z^=2nb0=NeFQX zV!-U<2s-kuLG0c!h#^xY+A(S&UWxk$%#0Xx@K$NPDB6PnW<7@#6QrU#F7i^t!9&hn z=4|Ikr0u6szkS6@7A2suALs4ch0uR%f9>+n6(sEwwn-Z+yu)`fehE)aMQ%HpIwN&}s1U 1: + raise AmbiguousNameException(name) + elif len(images) == 0: + return None + else: + return GlanceClient._format(images[0]) + + def getById(self, imageId): + image = self.client.images.get(imageId) + return GlanceClient._format(image) + + @staticmethod + def _format(image): + res = {"id": image.id, "name": image.name} + if hasattr(image, "murano_image_info"): + res["meta"] = json.loads(image.murano_image_info) + return res + + @classmethod + def init_plugin(cls): + cls.CONF = cfg.init_config(config.CONF) + + def create_glance_client(self, keystone_client, auth_token): + LOG.debug("Creating a glance client") + glance_endpoint = keystone_client.service_catalog.url_for( + service_type='image', endpoint_type=self.CONF.endpoint_type) + client = glanceclient.Client(self.CONF.api_version, + endpoint=glance_endpoint, + token=auth_token) + return client + + +class AmbiguousNameException(Exception): + def __init__(self, name): + super(AmbiguousNameException, self).__init__("Image name '%s'" + " is ambiguous" % name) diff --git a/contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py b/contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py new file mode 100644 index 000000000..35aac686f --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py @@ -0,0 +1,24 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + + +def init_config(conf): + opts = [ + cfg.IntOpt('api_version', default=2), + cfg.StrOpt('endpoint_type', default='publicURL') + ] + conf.register_opts(opts, group="glance") + return conf.glance diff --git a/contrib/plugins/murano_exampleplugin/requiremenets.txt b/contrib/plugins/murano_exampleplugin/requiremenets.txt new file mode 100644 index 000000000..4ea8999df --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/requiremenets.txt @@ -0,0 +1 @@ +python-glanceclient>=0.15.0 diff --git a/contrib/plugins/murano_exampleplugin/setup.cfg b/contrib/plugins/murano_exampleplugin/setup.cfg new file mode 100644 index 000000000..0e23e0b1c --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +version = 1.0 +name = murano.plugins.example +description = Example Plugin to extend collection of MuranoPL system classes +summary = An example Murano Plugin demonstrating extensibility of MuranoPL + classes with code written in Python. This particular plugin uses + python-glanceclient to call OpenStack Images API to list available + images and return their ids to caller. Anther available method allows + to get murano-related metadata from image with a given id. +author = Alexander Tivelkov +author-email = ativelkov@mirantis.com + +[files] +packages = murano_exampleplugin + +[entry_points] +io.murano.extensions = + mirantis.example.Glance = murano_exampleplugin:GlanceClient diff --git a/contrib/plugins/murano_exampleplugin/setup.py b/contrib/plugins/murano_exampleplugin/setup.py new file mode 100644 index 000000000..2a3ea51e7 --- /dev/null +++ b/contrib/plugins/murano_exampleplugin/setup.py @@ -0,0 +1,20 @@ +# Copyright 2011-2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import setuptools + +# all other params will be taken from setup.cfg +setuptools.setup(packages=setuptools.find_packages(), + setup_requires=['pbr'], pbr=True) diff --git a/murano/common/config.py b/murano/common/config.py index e37183189..df7d13426 100644 --- a/murano/common/config.py +++ b/murano/common/config.py @@ -146,7 +146,12 @@ murano_opts = [ 'Murano engine.'), cfg.StrOpt('endpoint_type', default='publicURL', - help='Murno endpoint type used by Murano engine.') + help='Murano endpoint type used by Murano engine.'), + + cfg.ListOpt('enabled_plugins', default=None, + help="List of enabled Extension Plugins. " + "Remove or leave commented to enable all installed " + "plugins.") ] networking_opts = [ diff --git a/murano/common/engine.py b/murano/common/engine.py old mode 100644 new mode 100755 index 3f88d3b20..a0a5db168 --- a/murano/common/engine.py +++ b/murano/common/engine.py @@ -23,6 +23,7 @@ from oslo.serialization import jsonutils from murano.common import config from murano.common.helpers import token_sanitizer +from murano.common import plugin_loader from murano.common import rpc from murano.dsl import dsl_exception from murano.dsl import executor @@ -38,7 +39,9 @@ from murano.common.i18n import _LI, _LE from murano.openstack.common import log as logging from murano.policy import model_policy_enforcer as enforcer + RPC_SERVICE = None +PLUGIN_LOADER = None LOG = logging.getLogger(__name__) @@ -84,6 +87,14 @@ def get_rpc_service(): return RPC_SERVICE +def get_plugin_loader(): + global PLUGIN_LOADER + + if PLUGIN_LOADER is None: + PLUGIN_LOADER = plugin_loader.PluginLoader() + return PLUGIN_LOADER + + class Environment(object): def __init__(self, object_id): self.object_id = object_id @@ -137,6 +148,7 @@ class TaskExecutor(object): def _execute(self, pkg_loader): class_loader = package_class_loader.PackageClassLoader(pkg_loader) system_objects.register(class_loader, pkg_loader) + get_plugin_loader().register_in_loader(class_loader) exc = executor.MuranoDslExecutor(class_loader, self.environment) obj = exc.load(self.model) diff --git a/murano/common/plugin_loader.py b/murano/common/plugin_loader.py new file mode 100644 index 000000000..943506cbd --- /dev/null +++ b/murano/common/plugin_loader.py @@ -0,0 +1,128 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +import re + +import six +from stevedore import dispatch + +from murano.common import config +from murano.common.i18n import _LE, _LI, _LW +from murano.openstack.common import log as logging + + +CONF = config.CONF +LOG = logging.getLogger(__name__) + +# regexp validator to ensure that the entry-point name is a valid MuranoPL +# class name with an optional namespace name +NAME_RE = re.compile(r'^[a-zA-Z]\w*(\.[a-zA-Z]\w*)*$') + + +class PluginLoader(object): + def __init__(self, namespace="io.murano.extensions"): + LOG.debug('Loading plugins') + self.namespace = namespace + extension_manager = dispatch.EnabledExtensionManager( + self.namespace, + PluginLoader.is_plugin_enabled, + on_load_failure_callback=PluginLoader._on_load_failure) + self.packages = {} + name_map = {} + for ext in extension_manager.extensions: + self.load_extension(ext, name_map) + self.cleanup_duplicates(name_map) + + def load_extension(self, extension, name_map): + dist_name = str(extension.entry_point.dist) + name = extension.entry_point.name + if not NAME_RE.match(name): + LOG.warning(_LW("Entry-point 'name' %s is invalid") % name) + return + name = "%s.%s" % (self.namespace, name) + name_map.setdefault(name, []).append(dist_name) + if dist_name in self.packages: + package = self.packages[dist_name] + else: + package = PackageDefinition(extension.entry_point.dist) + self.packages[dist_name] = package + + plugin = extension.plugin + try: + package.classes[name] = initialize_plugin(plugin) + except Exception: + LOG.exception(_LE("Unable to initialize plugin for %s") % name) + return + LOG.info(_LI("Loaded class '%(class_name)s' from '%(dist)s'") + % dict(class_name=name, dist=dist_name)) + + def cleanup_duplicates(self, name_map): + for class_name, package_names in six.iteritems(name_map): + if len(package_names) >= 2: + LOG.warning(_LW("Class is defined in multiple packages!")) + for package_name in package_names: + LOG.warning(_LW("Disabling class '%(class_name)s' in " + "'%(dist)s' due to conflict") % + dict(class_name=class_name, dist=package_name)) + self.packages[package_name].plugins.pop(class_name) + + @staticmethod + def is_plugin_enabled(extension): + if CONF.murano.enabled_plugins is None: + return True + else: + return (extension.entry_point.dist.project_name in + CONF.murano.enabled_plugins) + + @staticmethod + def _on_load_failure(manager, ep, exc): + LOG.warning(_LW("Error loading entry-point '%(ep)s' " + "from package '%(dist)s': %(err)s") + % dict(ep=ep.name, dist=ep.dist, err=exc)) + + def register_in_loader(self, class_loader): + for package in six.itervalues(self.packages): + for class_name, clazz in six.iteritems(package.classes): + if hasattr(clazz, "_murano_class_name"): + LOG.warning(_LW("Class '%(class_name)s' has a MuranoPL " + "name '%(name)s' defined which will be " + "ignored") % + dict(class_name=class_name, + name=getattr(clazz, "_murano_class_name"))) + LOG.debug("Registering '%s' from '%s' in class loader" + % (class_name, package.name)) + class_loader.import_class(clazz, name=class_name) + + +def initialize_plugin(plugin): + if hasattr(plugin, "init_plugin"): + initializer = getattr(plugin, "init_plugin") + if inspect.ismethod(initializer) and initializer.__self__ is plugin: + LOG.debug("Initializing plugin class %s" % plugin) + initializer() + return plugin + + +class PackageDefinition(object): + def __init__(self, distribution): + self.name = distribution.project_name + self.version = distribution.version + if distribution.has_metadata(distribution.PKG_INFO): + # This has all the package metadata, including Author, + # description, License etc + self.info = distribution.get_metadata(distribution.PKG_INFO) + else: + self.info = None + self.classes = {} diff --git a/murano/engine/client_manager.py b/murano/engine/client_manager.py index 3fa39fccc..9f0b7b891 100644 --- a/murano/engine/client_manager.py +++ b/murano/engine/client_manager.py @@ -47,7 +47,7 @@ class ClientManager(object): return context return helpers.get_environment(context) - def _get_client(self, context, name, use_trusts, client_factory): + def get_client(self, context, name, use_trusts, client_factory): if not config.CONF.engine.use_trusts: use_trusts = False @@ -80,7 +80,7 @@ class ClientManager(object): factory = lambda _1, _2: auth_utils.get_client_for_trusts(env) \ if use_trusts else auth_utils.get_client(env) - return self._get_client(context, 'keystone', use_trusts, factory) + return self.get_client(context, 'keystone', use_trusts, factory) def get_congress_client(self, context, use_trusts=True): """Client for congress services @@ -105,7 +105,7 @@ class ClientManager(object): return congress_client.Client(session=session, service_type='policy') - return self._get_client(context, 'congress', use_trusts, factory) + return self.get_client(context, 'congress', use_trusts, factory) def get_heat_client(self, context, use_trusts=True): if not config.CONF.engine.use_trusts: @@ -133,7 +133,7 @@ class ClientManager(object): }) return hclient.Client('1', heat_url, **kwargs) - return self._get_client(context, 'heat', use_trusts, factory) + return self.get_client(context, 'heat', use_trusts, factory) def get_neutron_client(self, context, use_trusts=True): if not config.CONF.engine.use_trusts: @@ -152,7 +152,7 @@ class ClientManager(object): ca_cert=neutron_settings.ca_cert or None, insecure=neutron_settings.insecure) - return self._get_client(context, 'neutron', use_trusts, factory) + return self.get_client(context, 'neutron', use_trusts, factory) def get_murano_client(self, context, use_trusts=True): if not config.CONF.engine.use_trusts: @@ -175,7 +175,7 @@ class ClientManager(object): auth_url=keystone_client.auth_url, token=auth_token) - return self._get_client(context, 'murano', use_trusts, factory) + return self.get_client(context, 'murano', use_trusts, factory) def get_mistral_client(self, context, use_trusts=True): if not mistralclient: @@ -202,4 +202,4 @@ class ClientManager(object): auth_token=auth_token, user_id=keystone_client.user_id) - return self._get_client(context, 'mistral', use_trusts, factory) + return self.get_client(context, 'mistral', use_trusts, factory)