diff --git a/ansible/group_vars/all/vault b/ansible/group_vars/all/vault index 0c7af3e..0fe0378 100644 --- a/ansible/group_vars/all/vault +++ b/ansible/group_vars/all/vault @@ -1,378 +1,395 @@ $ANSIBLE_VAULT;1.1;AES256 -64333032613165306433376335663162366661653234633037376532636563613831366365363533 -3566386139613261313236313831396138663733613462390a383931656539636132373135343031 -62396234326534363466396365333265353030393064383036373662623730616634333236363437 -6338623363663661610a306332393733633837636330653938643230613937353535303033643033 -35616632353337626561623064366263326339386335343833366437386230383163386666303939 -63626530383463326261353034613862373861643664356439306233303430366264626231373536 -36303862356364626164646432356563353335386162623539646632386638393234643638353536 -37656438343634356530626336653532343762666436383461646132353561353437343963323662 -64353435643761396336353338653066363566663632383466393362643861336534646632383064 -62303430653732303666663264333439633462656431636361386637376237343831353662613538 -38396138386631656431363761616234306664326265633432396638333938646164306334376636 -66623764613361336338646262636434633038353134373166306433323361306563323834356330 -39313030663764313137626535366561663365346536663034346163323932386232393364383939 -37613964323462313565663062346131323762323163626338346638346331616638386633636138 -36313938663939663734323433643261363130623362663838666564653663643263343139646361 -34303031646432323361366165616463646637663664356565636230326531653233333365383339 -33653735316132613236383131323032353934626265633338663131393638396335376236353836 -37366534336635366661373731313433353561363835613631383033656163373063306139323137 -37653766376230393434346137396537613733336134363031383332313930373839313235393663 -65326562383966313737626466663461326532636163363331306265633038313539303434316532 -65333065636531303261343164303862333031386361626431326363363261313635333430383530 -37616632643566373831656532373633343332653431353465333066613063396562393961363962 -39636232333532313836613930346334656563656532323562633563643766306139333034343436 -66623832373936396236333063666339303934303262326166616339613663346166666531306135 -33323163316131373231663935663331356362626364613965613235343139353563363234393537 -32396131646234393465623832333436623731363537313235633334326530393338313933613839 -31636364343961633564386561643263383031396632633436323736356233643261653637626165 -63613764653931626338613033376436363565666632643130303136326464326237643564616631 -38313766386362616364346236663138376439336530313132383235393532663666646663636166 -32616461663935363432353536303435326338373566666637626231393335323864656331663637 -34633835663338323763663835363466386237353833363061623639366537373465363464396239 -31306134666266386237343363613431623864333463353330623238613636373062653833323064 -36346562643662313533363134363964623565616162333331643433643863656232613935333938 -61306635636633333230623265356535656431373032363237343262623438393235653734313835 -35626637306463613437396235386338373763323634656665363864613361633365333738663034 -37656563326131326166386232323639373365363865353763613364646635303362393564396163 -64616161313864383634346437396635613431353838376663656165633438343430306230323964 -30373230613839626264643562633033643236623664336234333535626363646131336330323838 -32656266313066396130313865616531336461333361323436346166303432323030666366363735 -38663530343833396261646230333939666435346331356437366135336437393438353930333338 -38343935623466616562363431653663643465613763313230336237316265326165346238323834 -62613430313965326664386636323562613262323838386438356439326531313938613031393863 -38323936666335646461373936653065363139323431626662333361343039376534363433323465 -61663763623266636635346666623063666537396535386135666236653336666633333130383334 -36303439613565306663346164646162343764636438336138316261376130383331653266393039 -64393764366437643264373361373730663262396664333964393062663433653735623637336433 -36623533356361653465383737633238613234346464363565663934613632336638626238376664 -36373238336432363061633730356265636639343239653436396163363061613237373333666466 -34666435366634316462623662393131393138613961393030363831353832396436613966633731 -37383164366661323739313361656139373662663631643835303831363938313362326337353966 -37383139333532666631633035346137353432346532383866393938323936383834336231373830 -64376261653132336131323562383734653366643137613433666262666337366362333335633465 -31323733363332313561666438613930373862643332323931663438653666336433303034636666 -36636232386462373666356461616434643861663739303032383638393739633239376664396163 -36643130663963616433383235316539336133656562666134616438656532306438663739633333 -39646165313566386330376166366534323236373131616332623763623131323466663365646565 -30646435643737396665333066313035653361363462666639383538343833336363663264326130 -66613563336266316363353131613331386533316665303034393230323937396531373336373430 -66306632656163306532653131363061306362646539353837663634323662356536363366373138 -38333664663938393232353235343163376634373738313661613861316432666539653938613666 -35343161366638666439333633613632623562306164663934333737336362373366393335396563 -61643839663738323837613736316661383332633965333464653066303065653764393034396234 -31326466393135643765616165333235396466306366343934396439303337633535653033393761 -64336435343634633761373733623835393136623864323861396534336634633532636162376263 -66363439313032646462336664356636373563353137646334623939363231333639363838623432 -61616230636265303765313436623766656635633264316438373834343737363836616466393633 -37633232653266616163393531303034353932376464643265353832356466636534396239336138 -61333933366662333732366437353164363761643761303032333235306365653464383639316637 -66353231336139373435323264663863306135353961613437613736343364383463333031393030 -37613336313933343630646562303866363130383831313835386231373462326465623466396137 -33346238303433306235393538663532613662383764643264633038653934646236363338356234 -34343131383465306131316538326439656338353531326539626366323161313132666337376632 -30313262373261633161303737663162646130373164383030656437386363663534666639663663 -35356331383831386662386535393236636639646562383534393633363836636631396364336634 -35393734343338636161373864643666613530303832326561336138393536646565663230653936 -63373866646533346365393261363637343839623933356466366535363034363338353130376565 -32623530643032626261663864303362626461336233306164306536383565626139633164383834 -62646335653234366563636262643833616165663062316363393135346361623332663164616138 -64323831616265356332393535323666376233326333383139353837333762393437316265616366 -39313637393236306331313364333864333530323436303539346235333430653762666633643563 -63376264326432353639366165303537386535366236663537303932376533326561346533363535 -30396632626137303838333263313062313261366634653134626536636538623037646238313135 -66343338616337346434663735353138636132663535656534623432303661303130346230663930 -66626339646463643562303432313461636632313464663833353663393166323031653364613563 -36376439633731646332666539353465616230383433653765653835303566636664646162356464 -62363165623639626232376231666263353838386164336464393832353265363336666531653037 -34623738363037343461633035373135313630316639636231353835313534626635613566393965 -37323762636164383931373163316532633065386535303538616663663536663766646363356262 -34303036666631636365396637653561656237303135396136616464356561633738376339333834 -36366365353832343633333435303565386566393366363831643461316134356138666131663938 -38336139626562303837366235333038353739393863353161383635353339373032626238386435 -64363536306266306165333135373732336434383639613865333130343532616261376363323536 -38346563623161306165383962316436383631343763623336306435346636626664353434646530 -32626361343635343230373834373262306164313263396366626164653235663362613765376536 -62613531393866656530613165343565323839393861646337636234633964306130356263626533 -61383932333866663436653638643931626262386631386438333764663134656230356630396132 -64303332346366643130383664643438386664376138626136313163663961616439313165333866 -63336464653531366135636434313034646230616365313063613162633435346235656565636661 -34343033346261623966633262653762323037373339303962323735646366326131346334373736 -35626564623330653161396232323265643966343531633732383236316666343938343730316361 -32376536633331306631333865376662323966326139333130383162386564346162643833316365 -35373861383033333030656261393635386535643131643237343363346133306231343531373265 -32663833356234373038383634653438333864656330656531383632623439613763663935383436 -62363034623263613164306463343161663937613865363134323362313165363335616364393833 -62616139666337343632616565636332343064316666306366643931313639376133393735333161 -31663538353662656536623038623164656630343265636466616366646334623037393636393161 -30383432633433313737646565386337353063623761633931346535393662306534303732666332 -65323731396262303361353163636565303531643933353535323239353136313165343763336533 -39626631663238326663653530653965366662326537353038643665383361323030333532333161 -61393665616661376631616533346361663266363938363964333963383633363466326166393732 -35323762303137373566653433396265616431623032643734386136633137643732313930343536 -37666237386665363037326236623837626331333434623731366439373963316563643564303864 -36626635623162326164633233346363343333626262393531616461356333313733613165616439 -35623338336335356331633034623535643762356537313861303930383130316366363061383233 -37663136333133656564663962356535613333373932646239633235623239393539353137306138 -63333063393066616536656630613434653264313562393464323436663435313733366631623739 -63313236366136393365626634386438623062663338396539343438373162666132313034333362 -65633363643966333436303636376162316363643933316131336233636562363163643062356237 -35373061313139356230653230326366613665393036623634353563313530633036396461373132 -39323431353239363037356264366330393066653631656163346262616334653137653232643066 -61646132363331643531386463346234343038316565633536393036363564343332623238373037 -33386436313437313931343438623766663863343030633465356266373661313737663965656331 -65656562626236353866663038373535333262343031626663313439383137666131623833646363 -30316333363939633761383933666531306337303764343061313732316436626430313866633632 -36626136396362373866613436663639353532393361366230386231363530316566386261626261 -30626463313139336532346166303838313333393666656432306632316636323736653639366433 -34646432666266363366366561646462616365306335666437623639306539376663373138313061 -63306532653666336463313731633966366630613665353533383932663730643033653465336265 -62636139653631393335323238353537616664333962633632613233393634373263626333656663 -30643935663432633238646335336436313738323464656535643237313866386335643932653935 -32653434326365623635356134656131303663396462633962613337653837633066323865326632 -30626163313731326166313035396238343239653233646461353034653562343838663433646131 -34653837646431623661663364633732303934383537636133316230356136656631356166353566 -35303233386535366231306535323662303761666536646664396138373265643533373161616237 -39363263363065313832393237333831353836666335366365353939326363316530343665636264 -35373666356262306332323361333939636438323932373566373035333337393036386532633463 -34326135323165613263626632303664363333333262333466663030383364656666623466343038 -35306539636536353964633365623362323662613936386463643033666234613562386366386533 -65323238396532383965353663333565303134346432306135323331636636393036613031383362 -61616665616264326135656338656334353064346665346464316637343731303839363165313133 -63363366376234646631306665636237386133643863306330333238353366646665306262336462 -61663162383165306236393238643161383963336164333134376333653462356263316366386432 -30613163616336303234663835613938623162303764306335636264623230623863306637393537 -34646166353538386330616235306131646531323930363664333239323533363263653562656533 -65643162613539386238363564663638636132303638303436323137643164346531356632626634 -30363635346435393531323165316364383261333335623262336530623231363238643964393232 -30363836326461323730363534376437636161353061393030636334313430383134326563616539 -30363362316237653933373339393035636634373434636535383334393263623430643535663862 -34306266323630646464663266383861663533306563366539313361623532306437303865333863 -39613631306437373132313930363434646339303763356366663132623565303563363239623662 -38356165376362383735306439366630653962366465396265646130303735313836626332326161 -39303739383837636366613963326632633764393564623162343734663131633165636530633836 -39383430376666363231643538343638336336373833303534363163353630393032616666636530 -30373361373761346436316132323236643236396137653030656431333863373438386566363639 -35636230666138633335336266626530373762656439666133363262323932323665623434643732 -64343138633539636434373363356635396238333962353433653632316230313537393336396636 -35616239353838303165306232303066656131643532656433313766333066393162613635633734 -30336263363439373761313961366234323432323131303539333237313764313631646462366230 -31313763386262653434323734306463626364643966353064643534633031623962393533326436 -66616631333833363339623664626365313465666234363739636264616538336263366237373832 -34613431326433336330623637326335346330613962336263313032366234353938303238663236 -36663462383566363933666532653735666566353534353330356161383166323830356336366363 -63356330383538396133393162623736386135633537643037643238393166323431313239636137 -33306563383838633938363130633539363731653436643131646137393661663836343830343662 -64633365633862396362656565623439383139626433306331316665666534383063633932376337 -33313839616166663335346331613737666135613531363464376231353835613831613233373932 -35373731323334623836366435616231346630313862393861613162376161333138643931353164 -35383334623966356632373665663234363830333162396431353364653431383533633664343031 -66646664666565333763333266313164613362333532616130636561326164366634383137653531 -38636434326561383336386134656366393163623930666430326164393430366265376639356562 -36646338313137383436336665383238393034646364656462333430376538366265613736663439 -39636631633339643130656433303637383161323331353939323662356337626231323937613636 -66303935666235336466653131316563356536333336326132626438316338376137376337373963 -35333665383661333161333563613131343863323337366633383334336332383066666238396565 -38353332343761376165363363363763313431343031636561396534656262613761363665616163 -36653766343539363031303238313030663637666161303036353466306536303032306639663562 -36393331316434356666353762316234626364376333333966333933306636393132326464386465 -32393764393064656262623935326438373039643537313562646437346232353265333733653132 -37636138333237386265616234333361613835393433363631623534633533363634656165383931 -61666533313562663831353639663734626630323361656237383662666637353861613862306164 -65376565376336386163316666656564643333383731633562633834623464323731363562653033 -33643861636533613634343938363131656636656265633365633731653538326638343165313865 -38353738653830383737353433333830636339386339623435343330656530653766643732353131 -32303539356533373731383564616163643637323834333133363431666538393338393235636333 -36303062303138373636396335616335623862366364336365616633373966363563616439303738 -31636231343463623162613734363332323330343466633561333032663435643764616164366366 -36323336613136386662346164326331323034386462383862616166336361623937333638316264 -65643765353734383934343435636331346561636135346434623730386566323231343262393065 -30653364346633626165323434333532646664663333376362336165663431653434383865323938 -30643932656633633631303136323135323562306139643838393464333563363934323233343136 -33613163323135313536376132653464333730386462623131666662316139326463666433383637 -66313133643464636130616333363331663165376438333636366364346539346334623534303362 -30616165373836393038373430336539373732323631663666616232396666653364313138313836 -33306365366564313165646230326135633039323063333231316232316132623939386563396439 -30636661306531373139373061633031343165333339363035316338376165643364313862306132 -32356530343166326631306234356336623464303233613064393030646362643061363338393532 -34346233393739653732663336396230633066363663653064633132323235353030333539623235 -38623835303931616231316639396431633634656161326431643766306566396138653364346665 -64333666363739643233326566663764383362623263613538613134303461353563306435393064 -65323836373434313864373331656132363839393232303963346562623630646466316635633164 -64653830393361386631393336653066653032366263663337613039623935633938373232323137 -38663535653963376633623133343933333739363233373734323637376433633234663439313665 -36643038666536303539626436636535623762623230336130356231646665393965393930643037 -61636335643531383837653336323631333961353937623334633833653431353566336366343632 -36346439366535613532363630656462356435663032323931343930313235613137376465333863 -30336466366135613363633136383461363336336231656531373931313134633438633439656231 -64356333323737633965633633613939333064383765636465626265326338366532363562376237 -64663937633433326664313331356537353365663761373032363161323334383766356339343365 -37643662363266616535636365343261363537313765633031613263643561393663303339316433 -66383863323633643864383134386630333763633138353562363863653666366435316364643564 -35636230336437326561396536663235643130343063666165373530323732663761656131326239 -32633435336233333231633133633165333961346437343833633863373839316461663661373565 -39313033383562363063313464336631356630643461633065366439376636653339303362336363 -66353732623761343666666230646437636230333733303961666663366163313665353132333462 -38383661363431323330336564333166396331643664383937666232343962343635366432643031 -65313666383962656166636433323233353634313533643232333037303566646163393865656130 -33316630343535386663356665616132356137326666343134353934303336643635353463623635 -31623936353433626237643136313131656666623236643362636131373636346164366562393565 -34323065626565303035643463386262373164383431383865663536633362663137663437336565 -35616562353363653230633534393235336363363038356139366438316664353535333761363930 -66643266623862366461303735336465393039643866383764633937353835336137613865386466 -63613833393636346664613737323436333666343535666436323835386561373336636135346663 -35376335306266356564656234623534636561346235663137376532633439336231363563383234 -36386333353133623538636464613033386238363532643837626434346235353630396636343434 -61643764623133333861373636646531613633336334636261613562363766343138366563646564 -37323933653639343962393430623265613033396436383432353436353338383434373137373536 -66666162373166333937643735336562373637346130353262376134393233323337383765623632 -64313339323866333239323763303931393766366537323833653162656139353230376361333539 -37636530396136383030383235383664393566313363613461376262383865396466376264393561 -34316336323264633664353132363061303538313934383439346666653438393532376563383861 -62663932393431386237373237623063336237653665326664386433303538363735336565633938 -34613535313364666539363133376235383765643339316131323936323064363831353236643038 -36383431616333656639666365626665303631393133356563333335626535323534346539393431 -30363537353362343064313364636438383939376533613765333930653264353364356234626234 -32636330623737653832346362386537373032306238363466633232366364326139376234616636 -34396465383338366536363337313635613531343431623038336531343634616635313664333230 -61383561353664303931306631333164613637393632343337393562663864363962363966633332 -62363331653835353731663835323432333434373464663063306239323765323265316231663636 -38656137666664323163363639386366323736623863333131306539636436643361613137323738 -66636664326661376634303136366139373235666661306130383330613662313133393632663235 -64653363396431616338653938343531613530613364643861343465646132636630373366323034 -31323063393437393934616634646465306438313530656465323532356537316131626131313863 -32653536303066353764616235346434663063366234646266323164623962623861313865656133 -33366633636433393732346465616438303730333939333162356335383732626231353062613731 -66396436663639333262386235633936316431393333623861653836386164636438346565366531 -35393735353235623736613936383637663931333930373036396337383335633165613464336432 -62356532646439666361306534386239623634636532343434316336306663303339393537373834 -63666436613034343061303166326332643061346633363136656535613931623936313339393263 -30316133316431393661643336336463643932653034313637633762646338383965323032316434 -32366236373634316639303339363032663135383065376239613762336238313634386535666366 -38303063353062396663396130636131656333326630623339353836613432393663613261316633 -36376137633036653538323337663932333831323836356336376135326635346166336534373766 -36646630346363326237323135363832306566303462306365613863393735323862306537346436 -35623730323465356630303536373061666337326565356630646665613430636436333663333564 -65646435663238396436373161366631376137393533623131666561353037663135383165666231 -33663563363639346336343666386639323833326333313137663562623236643932633030306564 -62643836616533303665376566363034363334646261363565363139353031366362386433323030 -61663635653961366665666666393564366531633863393632393734383137376463363933393862 -64316335616162623436613365393135363664313266623237323830303632373139653763353539 -34313936366666303733666566363565363933316434333231653732663335363961386634373263 -31623862326462363364326334353038366538626237633138303362343532616232613837663733 -65393339643831323734646439356566393033373332386333633766373539616436623464373963 -64646535613436633733326335323664386262626437633862643664376131663965376237303133 -38383730373738613034643637633934636536343936373932656365613339616534373263333965 -37633466393734323431356239373038306131626463326636623063323930326339346239316235 -66643262303936306430306465366437346333386364663266346433616136343863383236343764 -65383331393739333131383763323762303766356433313334346137643963396466343466373637 -66336165356564366133623664373165666235306163666666633562623437343865306665313134 -33323133643066663134356337333763346162626437363763366331373865666539656462343139 -33333066646363663962303936353734663930333261373331353766633335633033653038333935 -31303631343663363837383962326137323432656230326439336662616233643766313366613366 -30663737363466333362303338653238313739316433343336366435333366393064616462363063 -62623466326133343130313836396233313236343461623634623936393562373261616237343966 -64623233653835613762363334363736303332656363326263323562626430656564616563346639 -63616562653964656262623134626233316234323065316464626565643561623863646464633663 -32363763316130393736636662353663316466663965353233636532653364633736633033356264 -63333264653162313438313531626335343938353361336563656639376365333932303563333930 -31316237343836386137363635373231326563626438663764383562666336626233643934336438 -36326365313033653432633430396265313834336633366161376537363235633732623036306130 -38303331343230306437386466643566326561616562623038363034633139313634623564313530 -64323033643965316439613938666166326638353166303163313163373635613435333036306536 -36363263326666323261633831626562343537646139303031336261666539396161383235623230 -31656263313130653637386332396432303534643036333761653965643632643164373637316438 -39353565356166653037373237643731653735333465313831393435616139663262616135383631 -37656234313838353832343035303732313663363034316536656263626265373632646438373732 -36666462626537663963623361313166376365643963383339353165616337313830343136386561 -62666637333765373663646534383466663830313366316133633364376537336264393062323830 -65396262303937366130333232323432626561353761666662393737353765663533646633343431 -33353030323365663936343433353866306235316637666262313663376430366534613364623238 -66383262313731396330623564343132393962356664376565626466613038653932313534306430 -62396564623664356633613030343439316235646665653034383464363736646433643463656230 -34393231386464343463333238663436376435343766343162303337313763623836663533393031 -30333735303531383231633839383037613834363265373436613439303266656636316139633165 -32393036343861353064663163333164336539383165313431386538636439333166333266383763 -38633539333766326136383962633536353432656663396638363439313137653037643135323438 -37323739666366353165346235383863633765346330303263656365353962396565323966653764 -39366461383563383762376637383130333738343564326231636536353331653365376463333335 -35336538396335313135376138613736313264663362393333383832373766326661343765643831 -38373438623530313962343065656339316131656363643564353238356639666566313264366463 -39373237363539313530666461653966303633383834316463353136613265346436323133623563 -39336665613764346335623133363935323434303138643033643737396535653261643261663330 -31353835353232356431633930653538316331623231363364313732343061333332646439353731 -34316133353564643534663464616239313836613133353938663164656661343763613433343366 -63383265303533383530396138363230646539643833633439366165343630636138643439393165 -66383633666433636130616364643731626461316331303563343865336566353134643466303638 -31666130656666663434306136323236613162386137353233336363316566323364623135653130 -34336431626139303634346439623362616464343963323335343030623835333964613863623766 -65396565343438366632346162663463353230306535643461373333613833613430343937626563 -65343631353035353731353766633630343936336161343066393862663536613630653261616435 -61643566306633653433623538313264333565303431343938386136616230356238363164613833 -32653738313465653435363138663061303965643262373135616663316166353961313135376363 -63353037643266636330386230636530636633356231313636303233633665363961303163646234 -32323664316465386339653730333332333631613631323634386139386230393465373063343036 -37393566636135353034366138643434613463653936386162316137343230353765316433626362 -64363362373465393433336361663934663965383031346162656234323630313338636331326435 -32656266393035616130636438393535626263653638643432633665323565616661363339343531 -62643365633532633262343966316530353966633263393432353061383134386432356665366236 -36616565373166303562396464356136353938363535663638663965376662336362653263326565 -30303538356530333237323935363930626664386264373937646336376532656532383234643639 -39666438313064366231336235373666616231386366623638666266386238343065363034303663 -32633830663662643136636536343536363566666333646331656530636635626534363964353263 -39353239383539363131313331626637626661306563643930356230353137653961363566663031 -39643431626164613964626231346638636132323462343933656236333964663962656534313236 -61636238386335633230353565613062363834633634336331393239373932633738383131323066 -61653038663631383539616438386530336462613063363137643533303837346264656630353933 -62313336333137343039353166393966323838333665653834636138346630353434643138373366 -30386339386439383339623934353462336166323961666432373136373863386165366364656334 -33666363633763643538383030383333616465303465333630383739376635613538376564303136 -35393564353739626631363932306131386535313063303638656638353338666335646131393131 -63643139643935356661613735373461633239356463666331343531643264386664663531336561 -61633132323637396434316535303638663336313162353738363931623461376136373438376666 -63646661653063616664616635383838333038356438313066383866343966343635303565633836 -36353538653438363931386261633535613066623462303262646136343461316465373961393433 -33653636383062386366353837336262393465333638663633316331656632653539626363393030 -61346663326233353864383063626461383966666331663438333739393065633938656236616161 -62636439663363353736343431333166393161633466313230393261376133373838336333616338 -32373039313135343864643661353263316335336537323436303437623637336336656664636236 -63626437343864313338313336316332613464633564653837333363373632616564663161663237 -31313466343163353936343631316637356339303563363261646366323333333237356139613932 -64346533383138633336363134663032333565363064353864376630366235666433363532356137 -63383965613636306534653836653164616638626164376166643136373566363531346437363364 -33363630353535626339663234303764373565616466636263343933376532316432393030636462 -36396437666331376661376261626232366337366665326537393166343630356462363766616264 -35313661633162363137666434643466353961633536623762383764656336666436646162343662 -37663032613935363261653165356632353730626232373934343061393633653564613133383838 -66623461393163623430303139613136336161636438366337373263306564653662656639616365 -38356664323437663264363431653562643136316362653235376431663836303833643737353361 -33643662303336636231616233393166393638353133306263333561336261303336343639353330 -35636132383734356235343237316230373766393563656663343131663561373630636438633634 -36373835363437386462373466386531663231363738616564313839333864326463373036343364 -34633632633832353363363162333536616332646630633433653663313332313531393363356132 -62663637333765633665616466643130363234626563636563643866343033316330623862303039 -39326561626331383935393662393436336239633330386261616462646631633964353862643361 -63613831323831336461313936356233666532333535393963353633366431343330626233616636 -64396464353563623037653163616535346437313433653332313862303363353638366539613531 -66616366386265376231376436366566333137646633323730303737653834616662666538663037 -64373139376332356436653733386631366165316563393536353564626633646365366562626633 -36373835623937386366356136613061306630626661653764653665313462373632666236323664 -33323435333062333831653434383331306333376432623330643062666130333461336630393561 -39383462626632316437343166396231386138616637306563343765626533653531636665393264 -38633966333633316339643265376361643366336334336633393430316432393165363239373332 -38326263343637663661663963303464636561623332333438306337343430643162626366643936 -66363166626665363031306161626263666531313733373833633433643130613039336539653466 -32323436323439366265656236623163366432306433323839343061376464633330376538626334 -32626436363463643534376634626562396437373037653962356262653939316562393233383735 -38643832386238316235366439646132336136363131326665653465306265353966623761366632 -63373332313766343366663137653131336339663032616435366362633638373033663762316437 -66353732356166376435316135646635636361626361653338343534383562383836343763363630 -61383331623534346334393065636136306339353433326436633965376363313030303661333935 -38363431363430323461353339643130393164326161356662343766616631326539346131666635 -35613462643731376336646338636464333935666666323532633730333162373865363333393638 -35316534643330613962656665303363353434313664663932666466616630653636316666646332 -38376431373363303931336437323432363432393539363461373135633839363334393232343334 -64353431363838313564613839623866653964383762313632366232393131323466656431343035 -66663635616131373665323166386364323637663061653734393039656232303434613865313538 -35336430333739356632303837323133376664643433303361396336636130636666373639626230 -34373761643564306663643037623531373163336131303733653739356362323539 +63373230343939633131333966383630303363656234366536633839376631323762656431323263 +3938623536303533306430373064316238633132323063380a646236643461373162316336366135 +33343834383962313838646461333935303834613162613633613034623563653333373431343562 +3565613135316434360a393239643065376566656332333562393737316432356633303032653131 +34346263653264623566333561363339346232373432373733346137376638643261663739623138 +38346634306362343761306530636138343530663563636135336431346135653464633564306564 +61663732353231313637333161616466613663363037313930646334346162626332643332623461 +35333638313261343435323638646565653236636231346230383136356538653865356563643661 +34313066306634396133376165323761333835323365663031653864373538353535643965366631 +62363831616664323735333236323734663361386365646636373030363639646666343764376638 +64623633383833653339636639666162303038313564653831303362306338623831303737343539 +39376630383937666531636530383137393264636464383331623563373764313232653839373938 +64373865646665646363636339613536616437373234626630636261653038663936643631383733 +36666137616631323338316338396364303965366639336562383133343131346162396362393030 +36363436373462643665343561336363653764393733303834643935616662633238343136646337 +36656233636362653863643335316462636138383665333932663164366362386432333933633664 +33346638323831663266636334333530356562383463316163303639363233353031626366346231 +32393865336265613562386166646437346630666432393237616565373563633131656562343266 +39373265393235646462333663656263633862323036373334323831633762373831333536323836 +64653035366163396138653866363765633865653637313336303064656131633531303331363432 +31613532336336393232613437666235633964376535636135656662353730353031353662613463 +65313163316137353831636633313962313631373733343839303036656338323738396364373565 +64353264396465616435333138353531383934363564646238633935623035653830366163373063 +36643733383536346665336339396135336333336235393961663565393166613563343235313365 +34363763353939613730643137333562353130616565343536396630666265303961363638323165 +65356138363864646136336665333261373333333234336237616138346332313138613233616331 +31346132376237313863336464313662396164303733636162626130393236393633663630633733 +35343866356230363262326436313738386334353565363265633261303031333662666564616261 +38623639623431376232376566306362363734316238653630353564623961326331653464653264 +38653437333830346461636630363538376137313030326132313665353065396464616135626265 +31613433313866333931636535373063353865356435366132383538316265316635326233646439 +33346439643262613562363861313364396537366231373464396164666635396435333132393363 +62326630303135643264653935316330396664653063303934613335613133333931623262626138 +63376438356539363533363134343535386165656364306233353364353364613665643436386163 +37396164663630633233343130333036323730633532336136613862376134356534373237343739 +34636162633436323465643165396162376364376365616236303361333330396238386461626362 +31363931663834363464343866613862663037383135646130653939323566616334313461353730 +61373935643962633138383832656634346563666637613434633130663665393637353230336633 +37643534386136623731316665663630303634363132663636393862386666633239363937386163 +61313361663862343438613835356539373462663230356166633231613064626638653532383537 +66663331306639363932323530343164343666323237383631313033323461373432653063373665 +65353531653565373165666666396236653038386435653666376661323164383537653936373432 +32643234663161316564636333333265643661633634616235346132356138363966653430326634 +62343237383936656336343139373135313830613466373837356534373666303233336564323466 +37313266316461353062366536323237383165396434663861653032383033623964643965366266 +31633238363865623237626131633334313139393437383163313762303264366433656562393461 +65333039663437366131366137316230366261613131343866396131626464336165306431373931 +36343134393238323165343631373032356332663337366464666530653665313864373463373031 +65393164613637653435366164613336386138643463373433636637383433323434623539666138 +65333335343033356237373439303636313639623231316638393533333838636364336235643437 +61353234633938333263373866636161363166623733356533393665303632643635653131306137 +65353531656465373830336163633564353665306566636232323430653363343930636131633735 +35343335613531323032346263323561353636613166626533326663633133363465306431323039 +30326561363764663435666262636664326438373536663464383232313636373466306661643537 +30643232373636666462313730396232653865366235313237373362333135393435636632623638 +61353735386430376133373333323464386463303534316333333438656635336636343130393337 +33313137626239373731396438636434326133326332353965346138303934326231393066346439 +35373535626134353037316362306539396234613736643132623234393962376439643134316163 +65373331336364393337393339343433663136396162633263383535663239613132663862306363 +63646662396635653862646137613964616662396630656366326563636135613337653661636639 +36353561373035386565326666633366323334626663643366346261316365353863383037303836 +35356562323966323739396335313736663763313463663465396430633066343930363961376561 +61343962353239336434636634323535303534323739326239633965303065343936623433393462 +37646135653239373165333435343531343166613864323238343664363137613964396130316439 +63363963643330633363313631356535613761646461313632333366626237333732316537653136 +30633532623132393764666338306262393562366266346134356134663661333030353333333465 +36613333353932396439613030393038626333356230636638666663613634333365376561376439 +39303264636335346630303032346233363238323032386362323433373439666635313861396263 +62356239663734626665353061323131623032646365316338373235383139633334636466653430 +66633662633430376331373233393532666663636636386432396263366238343663613136303263 +64393431623535626161666165316235656233616337636663363762343665653236666636323063 +36373364353566356438663764383662623331303063363866386134303930656638346463336238 +33356630356536373062643533626563376331386338636638616234656535353038323065333930 +34613764343832313063323934356462623065303465373739626533336465613836343738396463 +32316561343236353365353163323938656364663634613235633165343461393232663335643865 +34346130336539663339633266333166643937313761636364346563363263656661353639306639 +36336334313732333362326432323832626235623331323734323539333631373535363964343037 +34326532353636346566636139333464616466623234346336363530396136636139303830613165 +38653539383039363233316437373463623130303038633233373565343632613239653630326434 +30393564373462343361376636313037313961383065353864636532643337353162393661626630 +37333363313730373132353330323533303635653763616263663964333763666538643864303231 +37333662313837353131363962383735366531333730393534396263396130343336643138646234 +33333434623966653664316663653337643266303234623837613962356635393865373331666237 +64633430386534663162303363333861613338373835643837656664333364373361636338633761 +64333034623063393166313336303431383261666564336132326334366639326363393832353865 +39633736326238663634316663346239363935396432313336663037396536363561323538666364 +63396230333066626463626331653438373365656435333761343565343064326361343537306238 +66316663663963643266653132656439376665306261626665393061333962373133323264623330 +62343033646463383637636463613633353465633738636465346135643738663535356238656433 +34376339613566393764663264373034393065636536653837646466616563656362636231633064 +36393736626462303039623932613462636635616233336337363135326564393439663262343363 +64326630613563623164366337303430323233326237646634336335393664323231373237373137 +39633962366331373531343065323563306662646530653535636438626538393537333033343462 +33303332313135653135616464383539333261333730373362666163363335633830376131343161 +62353161383664346337303464646463326464316131393266366464613333653266326161663539 +32663037366433393864666266653137373331616339396634626262376462373533393761326337 +66333666376563353839333432336563326462323461313635343231313964386333656636353266 +35323062373131383661346431333665336237313036393837343933333631626236646438346136 +63613231393266323739356566663264316536383632353134393561336266306361626532663966 +38666666303632346163333066383934303765323839663839326531356363643465343363303165 +39393366366237343565653831303335363162613461623063356433353565643233343134376435 +35663739383135313130646663383662353561323930323366313962386333343433316236636462 +63396563653161323565336433616265623462393761653935663438373235626535343066346331 +30636366366434306563663036316436663465313330386231633736303562313239353736393733 +35343330636539386132303464383134623063643563313435386563343261316635376135363832 +33633737333930306439656563646366623461636366653366663933396163306138393765666337 +35396332653563633330363037383734353965623763653731306532626238633736313361386466 +38326362613161633465343437326337656363666463326164336137356435623365363034666335 +63303938343631313439666538633731323732363530356634383131373766376536306665303132 +30386137373137653733633866346439633530616438343638663665323132643335623563393633 +65623236363333626361316533643134333864616364376664646264383436623264633462366161 +33653131626335326437663537393935303937633932306636333264376137353262313237326538 +31366166646536303931373464633365636666306137306666396231613465326235343162383331 +33656665326439396231306538373232613535396233323361396164633233616234623339346433 +61653034333437376434323034393637396563336530323961663862383334383563343033343863 +66376537386330636139343831303235663962336332383365333439646136363632383838343839 +33386265383336316231663665366638383138653765323666303832326537376139303835323933 +66393638383335336463633235633731326139323266353636636239313335323430363066633430 +37663535663837316338316562353835353231333063616635353733633939643535666236343137 +31303765616435643765623837653436613733663135393630326264353464356366303866623564 +32666533656466643063663165306266643164616562326432663564613733386631396664306263 +33353433633461623061376232643963326463663165613065393063666437653236313836383361 +39303063323065376239623736616566656532666533363630373961653662386538323130323331 +62656433303166333464383065636334316138373362323838383132303237363632376131313036 +35643231313338656562303034626563373130313136643463396236633337323137306435386262 +31326362356133313263646230636132306536666562306364366337666163303334653065616134 +32303437663730643836356461333436643239333262363733353338353831323566323438303162 +34366138383730393433333662626564313538643838633630393465396565313339613864396536 +61366534366530376361393930343236636630386261343839663031386532356533383433383738 +37313133393130373664623737323631616138636665653431383435376338323439613737326535 +34323462303639653162313431313737623239393037303431373039653136386539653138376464 +33613461643132666230653932363739613930303333323739343033623936636536656139316435 +33623964613833383030333937613037333331643230653633633135303561393337663435393035 +37373135663461623663623634623930663937366162626134383862336336636633336138656334 +31323565396531323633393534326538616338313030303230363765313637656232626665313532 +33333662363062313530636661646536383531643335366561366537303263613534336537663764 +37376238646464383466346130323232373736616636333939356466353830623861653664363235 +33366561623531343462303462636537613939646465323965366638653936643363353164613030 +38646164613330313035396466653931336237326464646532636133326234646133303161386632 +37656438666663383430363834323966383633333438336138626166353339323466386566343461 +62346566326638323730343839363730333736333465613933303536626132366561313231383434 +31643732356530646634386230336566663461623462643036316433343334623035353036383234 +62636133666431303163646133633966383030666664363630343730333634663463663034363264 +63316265653034363239353434666263656463323136663132663263373130303536636266383533 +66353234306430306161656164313131376163646332646435336664613764356333316666323832 +39373932396530613430373130646464346463376330333566623935373433393265386663376237 +37613935653933643435363932623139343738366236346434666638323437383562626133353231 +66373166363138353239633134356338333032613338326633356139353232326232653963353864 +33346236623333626635316239346236313765373863343830363434333738633532343730303462 +63613930376566316463383936333933396566626436383062323834636362396361383738633336 +63333564623363623738393835316264313630383631333638333066613963343761383338306566 +32623336383933646265653538336566323430393735373063636438396539643365366134623864 +66616263616663633039303861316235633934313334613333336163386364306632626232613834 +65376265323437346635313330396439643536373564333231313634323837613066373964316334 +64643634363138613465363037386534346164323134333138666261623234386336653664313562 +34303331663939623833646662343063303865346266323430353232373339356336383338363933 +32363739323330333937346334643031306166363132326363626261306436343065303339623963 +33626636643865636461383261366533646139333138396232376339623066313635333963343532 +37306463643263333632396334373739323665316338383763643830653965396539366432373164 +37373335313137383664396563323831666535313635313737613365626639623864376163383436 +36643061386661383939666630393133636134316535663934333766313837653332303531623437 +39363161313936393361343265393132393636376538636338646230376564393330356534663263 +30646334343961366561613661623036303535313562303666376635643431323237313132373931 +32316333333237636330376166326434343438316362333464343232313232343130376533353235 +64363761643136653563353232653239646238366330313939613436363465353061303033663464 +35383039626166363937633232613836616164663832663938613237393666383336366231313438 +34613930663536336263646637666361366235633066613236383463383464616432616563383831 +39396265303364353363316132313264393735663034616561363062626530383463393761363931 +38656261616635643836313264396130336538643865643636316462336663643066393835663339 +32313263616138666461636431316337386530356139666139333236613964323734386566636666 +64616661303562633063343464363766363132636135633463633235356161626330386364646336 +30643165356237353130623534616239336632353237353263393837656664646537633766646531 +36633433626336386334323039303039623062306637633539656339333233303262633033313639 +62623062656431613238383737353239326633386237653438613166386333653735326661333838 +65666239643435313237323032356261376563333265386133383237353034613537623131363038 +36363066346135346364336234303539336638333962656165306264343438646166313962643935 +39623764643034663965383865353939306136663532353932333664396165643830333030373730 +30613462346536656661656534383864633664303735393035623964366632306233343165663038 +64386364643630653737666666376565643639616639393462373233333765343961323631336436 +32333634613136663734663763353162356337326537653766633664353163363634396131373166 +66343933343862323032643537333464303836623939316130623961323037623839663639393966 +32623439333836383234313135306135363434656366653964336135383765383163643962663465 +36653130626362636561646661376139356565356563373435626537663930643837376530326662 +61356336656235396333663362326563643039346231633261323930653162323938343562626235 +36663662366534383932396630326338626333666437656264343764316339306132353265613063 +62393339653630623965636331623331633733653530653164643863363637616131343036633432 +33633161396331333264386431663565636334376533363735666466623830383164356366666136 +63353061623564386630616134613866623532663237333432613962366537643834306364316632 +34613465343532653261303664326362663938613638656532613638303566333635363263653935 +62363363393539326638333965643130383139313935376436363831653837303831326634656639 +31343166613030616166393638643333343264626466303363313938323966623832333330336436 +35633538636437313761383530613862343033613334643465393164386431303138323239313436 +38653161643261326438383132306663333235336362353439306566306563643664323335376265 +32653365383863323165343366666639393538363434353061623939633262316131643465316462 +30383239613439343333636663363366616263393866636466343835333335633030633062323039 +62656564613065303336323064616230356564313235393030316239336162633831363336623261 +31633163363937346534316136306532636464353463326166383239383736363962363465643162 +33383539396364303062326666636437373761646462663338666436656362346231666136653232 +39353538313663373066623935623935616461646266616231343338623462616663373837616162 +36663436363532346565323363383263376231356138646563333433323561343533333566303431 +36633432633039396363666634333862613835303838323733656332653031346637316233313963 +65326662313564643564616535383264323137333136646662396462373134316238323537323538 +39613734633631346131653336333562333831333030333639656231303261643034353430613933 +34636164376165386431636334313831323733313266323666656365333062353063333839363937 +65386331643732653430313138343639366437613466316438383762376164383031636232386162 +39353064353466373664653437626461316563613161383461626331393363653332643062626331 +31656334623061346337303963323630613065326364343834316361373364623566316630643036 +30323334636137653439656235396266396434663737396466366363646664393162303563626632 +35666437353665343065396536353731353532323664363430303934343933623566363031366435 +65326434656465366364383932353131353938326535366662633234653038363164323563633833 +66326530343339646161353766376565336366363364363231613031663966396632373539383130 +63616333333736333563343232656162373861666433393462623232393535633235653237666233 +38373831303862323735623461393365303063353235326166613163323162613164613162373761 +31663031653835336231303432633735653538303062303030323964616439393538356138636539 +61656136613835373965383837616366643931633933633733626136346536646630393537626633 +64616563363034616532303936343136616662363965353739666233313936613039613235333836 +63623031323430383039353939333934346638366239346364383366383966393364393165316633 +32343566303766633464303164323339313130373261306564393737623465393631363231343138 +30386132636232623765666462653638383330373438323834326563666330346262623366636463 +39626633613436316164353861303333326564393864393662383263343662616633386565396137 +64393337333634646434326264373335383762356630383633313539323737643736363262346464 +35313230323132316464616332646333306239613762346439346234653437376164316335656561 +38396264303936626338313232323636336430643761363432626266626233633438303535656136 +62323232393438343661396536396261643466346339613866353037303732316538656633626130 +35323164653337316636613163316130373137653134393633613936623530306430663435383530 +65653461323833656466623938356335386235643236353662653864643564303262363937623363 +30373430343933616238323434373039356439316531623562653862303735383233346134363338 +32653433623162366361303565613537336462646338616337656437616363333936633433356434 +62336664343063303035313233613765336539666437623635396366306434363231333633626561 +32623731663465653237326232346263653163616132623035313632306630663239626339643530 +36366438326261353364353433323630303631306336363564623237386130323663616334633063 +33643665306138393434396135656534306639373133323866353261323262343532356439376435 +38316239353865366239656432393830353563396565663838323432363333323533363232643666 +35376634313338616436303131383838396166303864333165316262373538313731323563316536 +65626230656339653538343838313631336131393533626265373731326536613861336335316665 +37373238353837633838633136393164663937626262613035646263383639383365313865613365 +37313537393332623535303038666432383432613363633462316466363436353231333830653137 +66346633666132313136323965613865633031346134666335356237353261363937613637656333 +65653537303965383133623161343332663962373462326232346334313064373530646331643838 +63333538636561336335323665366439313936623136626465303630373962316130363461633565 +30653430633733376336323537633036383537336665353735376466373762393761336535653836 +33323935613065336535323034343535323531633537386337326665343935323136636464386636 +37316561626664386634346435613331613363383461626333653738323130633633366462333465 +36383237616363306631623066616534333866383138396435373839366565653738663039333266 +66626466663834363766313165396132396334666266303665616666316236306433376536313765 +65656530656539383833646430393762663066396430663838303566326465633231653039613134 +62613030663037363062393161363034633831346631656262623330333032613030303138636161 +62366366613038303766373530333039376266303137366365306535393238323565623633633962 +61653634366633393630373834326331386230313136393966303964373964623561336431623433 +30626333363338643039626661653437336536636331396132383834613264343965666134653733 +30303030383832336463616164633866376332653735306436343136376131653231313733363065 +61396639323539363763386236373433393032326266313534663566363064643765306134356562 +35346237626439326464333330663166666466383037303135663336613139653330663039353538 +63316536656162656238363235626637313835643062626163373839333462353239633634666161 +35363430373464336635656361316266333132396564333331306261323531306561356537653837 +33653365643864626639393864393665363739333239326639666132613334643364663431353961 +34393363353234616166323536386365316462386339333334663036313938633364626236613633 +34666232613061316661663132343032623435663435366539663532653262313564613938626565 +63656430343165666435336461323966393565373931373837626464313261303263396161616565 +65393366303739623061616633353664326362376666346166613462626536336363323164613835 +33326133666166333036326162366233653664383961363239343865313231326334383837393039 +32633634383430333963633661613563326566306433653233313332393235666536633734323762 +33326135363231303166326632323435363233336132353666336235666136363531353865646566 +30303330656665396634303330313463306566653863663335626337343562373430326633653961 +30646366633330333431356566616336323732326435356336653164323238373863396465626235 +65386338636132383632373764633139396265376566323037353538313835366232356366393233 +32313236633831326136346232623630373039393862613435316563343966353663666364383733 +65336639656438303835393135343431316130326438373237616666366163613863306264376438 +37323339333731666265323138376664613065353533363638386139346131613065353663306334 +66353932616139376330366364323564396664373539396366653239383538343563353832633039 +31633330343235343138373365363161303966316639623961306433383037343735623932376430 +64393932326431323938306536663239303130366265323965653164326532326265313761323337 +35626232663834393066646263376563333635643337356639373036306538383536626363396533 +63626361333861643339626634356430323537343865643535643238336366393835623130383337 +63353139306338353961353732343461323964623532326161636162303038623661633436616134 +62333966613432356430613961343837326661623130356232623633363865303039653832643638 +31643035613561316663383934343262653661636333323963306330373133303431373433353934 +34376333373166626561616532356266316666646536323963326662363335653131626661666436 +66636562656431663537623339616137383161373862323362323363373634353861353035303935 +33373963346164363038336164343733336265623638396239373263326435363662343232366664 +62383039646463646263373463343531333938393730383531653163653930313236373337333638 +33393632663439643364353734623530386239663430383263646266323833633632323238623762 +63383464323230333039323838366237376334663163653463313033353664316565313263323861 +39356430623364376530356233623762643163356234353764343461356238353337333634353939 +66356137633464326432363433383335313938306530326635356333353732376661616465373661 +30666462643339656664356431336661643164393933646531613266656134393163323534343166 +33373736653333336132663038663561313563363562316363626535383834356564383535343962 +63303734373735393162326336343338336238613038626231653936383264626238613535646164 +65353366383134363239663738363433636238633038363735633638363837613635616337656534 +38623438396430336563643266643434336130663337383835383939343461373962633731623339 +65663636326534653533613436373635373864616165393362663033323361343735633863353837 +66343037303437346461386531346363393965633165353062623866626663383838613861653332 +62343766663237626266643166376239343334363536393166366138616363663766663136346235 +34623362373364316230386465336264353666643934313165393632313337393164393165633661 +30333638633432306164646163633561373564653436656330343439653062646630376536653362 +39356362386639353865383730396261383364613964383665363766626639666630396237623733 +33323939376633613036353066333534396163633737363864623634646366323032393635343434 +38613263366331656363373239633764353230306338316635373965373837386136303237633334 +33386237366166313766626133313466323635336437313363393061643533393866353336613961 +31343561633562656431313131326464613439636666313537336533333935323338623538336230 +39396534663065643232363339396234346335623531613437666136323531636639623739303434 +34653063623139373065363438653461333239376164363462343737366330363934336665306338 +38613863326362646631636536633561623034613939306434306431646261343563346664333132 +62623431303537313164633333373030623630313733313138366538326361346534373634363663 +65643536616637623734373863663466363066323465633862333538613935666337376530386339 +32356531326661656461623434366666386134363336363766653462643031626238653063343864 +30393232303534343365656161356162383735653532636538616265333062373036326164306634 +30333035663434386462326230303231366430623261353439383330373566363238303666653637 +61666562346631393762383565333030343263613139353565613238393337613338666364656538 +66336131356333303835366539386239376332393636363339323763646138333737616433663230 +34386439356236656330656234613963336536633766316232626632626338656363383638313964 +33633465383833366230623563653332616233666335396530313764336662313436363764636434 +33326566396538333961363836383566366231613839386338363037313934306566613235303930 +65656130343733343963343930616136306632656539663665613334613931383031643661646334 +63326530623639653732326463313966363231393733323936616565323462373034336334303166 +34313965366231643261643161356361616431623931366133386332633735356266316530326537 +36323539346330363866353764663636386435323063386535316139366634313331613366393237 +33653965663737363262396135326632616536373331396431306664373962633862633666616237 +35353439393863313132323533623431333564343565613633306461373738313066373836643461 +38343731653433643937636337636436353237663765313735646431303834353662326431303163 +37613762396639353066633636613836396135376563643064623039346434393138383166646264 +62656461633366633431363831656366373934333838336463616264646636653630396334643132 +63306238373230613734356536346439353738303132366265346561326534303239303039343832 +63636361653837383766353136313465313164653534663862353966333339633864616239613937 +61363062653833316630303536313765323232313633356363376436643230623034323264313065 +66643061336536353965633038393231323965376637653131383264323463393238636365623061 +33316432336134633933343566373561333339333764316633393762653238393630343462373433 +31643863323430626432383536326437613639653565373665326133666666326237343039363163 +33616162383764613039326438393962353563343166373330343862633366356232363765666336 +36376634393034356130326634373961363730303531666666353532386433373932656332333136 +37663364386262363565346566663161396661616664353035386537323230656235633866613432 +61633161666364623033626438396534313931353432663435353036376433356665646266306138 +36306262303934653231303631363364336632353739326338616638616335303332396664313862 +34613363626438313337316632313965336331383362643266626336353264346164343036616265 +65373462363732353534623239306230346532333366636530356531656232366239643335646164 +30353266663833646464646165316664616564313734323131386236353535336339336635653462 +38303430366538356163373331373633336634343238393638623434316663666336316163383365 +65646462653765633961303438626665306139646462316630663635336630376363343337393034 +35363836386365633161383463323062613663373431336139656361616333366534666666653735 +38653931343961313565626233633433636632333835643936333739626365633938636463316637 +34343965393235646564393762353430396133656136646565323632313035623736353730663761 +61653539613837323862383136333734646134383865666465633938376661616137346564653262 +34346365356663626634333032383763663434656464373764353061326265646330646238333137 +64323331623838346432633964393433383239633731366365643234643832376161376430653837 +31346138366466393839346231353937626431323065383837343062636664626266333734373430 +37623461363062343563313930316131666165633463616438623263383764653137316461643738 +64333431663439383163343438386637653537303835393930326230363166306136636663393161 +32353136393663363639346338323933383032383031346237336437613061626638373330363639 +31636361353233653034613366366339636564353639313038643832613261383133393134303136 +66326638623238303761396262663862373161323665396266383738343330366431343562313530 +31333730393634623466356532386138663939623230323531373566356431613031363236396234 +65306164336537643636626461333234303266306433646566363361623234313735653930343932 +63346339633864656538323631363231343930323631353231646534646339323130323237373835 +30313834613334303539326338623735396537316162363861626566326662636638373931343134 +61643530363465653934366639393364636161363633336636383136333363303936333333303261 +34323231396365376232653733386464333364623634353361323635373935366266323130666633 +36313530356236636534623661343335306330303961336138306335613836653938303966616231 +37646234306530346234393439323435623164306538396237343138313131386239613834323664 +31356663616630363536613037633666636163333034663132643865626434633863653463333739 +66656536643731323839376239333665353935343563363436323266343863346466363663613336 +62373064633264643932376265653162336661313133326438303931646135666161383165626262 +65613132613737626462613038383234643838653462646364373166323064383366333933643933 +65653734343638303235336561366131336630353761373866396437323864386235646665656334 +63666264636238633938653535376431616539376465303964396334323435636564613562643462 +33396239383132626366333466393135313834383038383361383631616336613166663062656634 +65616538303165616637356464353039333765643736313761393237643933643736653436366462 +63363732363864336664363637643663383732386537396231303565633036303534383464336533 +63613334336138353632613764663962346263323536343532643563666332626531653464333931 +38333734303230343533646238323865396434346232656664376465383938343139396534303838 +65333361346234663133383837646632393939636365383966386364616433636666313466633138 +38393835323263393536336439646237613033306665336133373836323539303033383461666333 +38616631656430643333313634343061623934633166623262656133666135646162343363366539 +35386434343931393432313061386632613632633733646437666265653130363935666239393735 +35666633303132636134626365376633386531313036653536616462373164303734303836303936 +66333338613861323266633339383539303238626230613663306438386333373831316135303939 +39316264616663323761323130313930373834666161663836333735306338633232313637313361 +66666338626338343434653761363735613166356632396663643332616631353433306566306662 +63363538346634376338313538376333376537316532386531633537396638663732666665353462 +61353339356461323532313630663135626532306132336161373235663033633033656461336235 +62613434623631353336376230656232363730643637646131626430393962616537306262386565 +31303839323263343439623935313666303330353539623130653838623161323130373933383039 +38336631343761633861383338666365343562613835313265633165313832613665613230303533 +37646432623461313236373137363536613565353164333935346337376635336264623965346337 +61646462653032333362353738666238323861323835306664303735326162356338613162353763 +38333035323661313661346136663437613931386432376165643830613066643061623934336130 +64396431646533646130636333363866396138323131383037373937383230353662646634376132 +30643530336665663865623165653135363961373838666134346463343239353733326262396630 +34373562646634373566373238353836326562313162663764386239396364333162326630626630 +31373962643765663364333766373031646263626236613461343533396164663962633235343136 +62373936646332303161326265383165353362643234653831613539613231623563353566613633 +64316461616236313137353136336365313833643333376665306232636537333734343633613461 +66373066393639623037323535333661656664383265623130656563383537633131336562306237 +64366135643537653231396238313836353439303465343732323533356437353935396139376462 +3839656237373031313431326364373839613265303330383236 diff --git a/ansible/host_vars/illuminations/host_vars_illuminations.yml b/ansible/host_vars/illuminations/host_vars_illuminations.yml index e08f54b..ccc889a 100644 --- a/ansible/host_vars/illuminations/host_vars_illuminations.yml +++ b/ansible/host_vars/illuminations/host_vars_illuminations.yml @@ -1,6 +1,6 @@ --- # Paperclip Docker deployment -paperclip_domain: muppet-vision.wdw-labs.cloud +paperclip_domain: cranium-command.wdw-labs.cloud paperclip_base_path: /opt/paperclip paperclip_port: 3100 certbot_email: ryan.blundon@protonmail.com @@ -8,6 +8,12 @@ certbot_email: ryan.blundon@protonmail.com # vLLM inference endpoint (local) vllm_endpoint: "http://127.0.0.1:8000" +hermes_model: + provider: custom + default: Gemma-4-26B-A4B-Hermes-VLM-MLX # from http://fantasmic.wdw-labs.cloud:8000/v1/models + base_url: http://fantasmic.wdw-labs.cloud:8000/v1 + api_key: "{{ vault_fantasmic_api_key }}" + # # DNS entries (if using your existing dns-manager role) # dns_entries: # - name: illuminations diff --git a/ansible/host_vars/illuminations/vars.yml b/ansible/host_vars/illuminations/vars.yml index b77e425..25fdb0d 100644 --- a/ansible/host_vars/illuminations/vars.yml +++ b/ansible/host_vars/illuminations/vars.yml @@ -1,7 +1,7 @@ --- # ------------------------------------------------------------------------------ -# FILE: ansible/host_vars/astro_orbiter/vars.yml -# HOST: astro-orbiter (10.1.71.130) +# FILE: ansible/host_vars/illuminations/vars.yml +# HOST: illuminations (10.1.71.130) # ROLE: llama.cpp LLM inference host — Ryzen 7 5800XT / RTX 3090 (ATX rebuild, # 2026-08-04). Superseded the prior AMD RX 5700 / Ollama config below; # drive was transplanted into new hardware, not reinstalled. @@ -234,7 +234,11 @@ vllm_service_enabled: true vllm_service_state: started # Paperclip Docker deployment -paperclip_domain: muppet-vision.wdw-labs.cloud -paperclip_base_path: /opt/paperclip -paperclip_port: 3100 -certbot_email: ryan.blundon@protonmail.com +nginx_proxy_certbot_email: ryan.blundon@protonmail.com +nginx_proxy_certbot_staging: true # remove after first successful real issuance + +hermes_model: + provider: custom + default: Gemma-4-26B-A4B-Hermes-VLM-MLX # from http://fantasmic.wdw-labs.cloud:8000/v1/models + base_url: http://fantasmic.wdw-labs.cloud:8000/v1 + api_key: "{{ vault_fantasmic_api_key }}" \ No newline at end of file diff --git a/ansible/playbooks/day1_deploy_hermes_docker.yml b/ansible/playbooks/day1_deploy_hermes_docker.yml new file mode 100644 index 0000000..1fe4fe9 --- /dev/null +++ b/ansible/playbooks/day1_deploy_hermes_docker.yml @@ -0,0 +1,7 @@ +--- +# Run after day1_deploy_nginx_proxy.yml (the role publishes its vhost through it). +- name: Deploy Hermes Agent (Docker) + hosts: illuminations + become: true + roles: + - hermes_docker diff --git a/ansible/playbooks/day1_deploy_nginx_proxy.yml b/ansible/playbooks/day1_deploy_nginx_proxy.yml new file mode 100644 index 0000000..a1e08b8 --- /dev/null +++ b/ansible/playbooks/day1_deploy_nginx_proxy.yml @@ -0,0 +1,6 @@ +--- +- name: Deploy shared nginx reverse proxy and certbot + hosts: illuminations + become: true + roles: + - nginx-proxy diff --git a/ansible/playbooks/deploy_paperclip_stack.yml b/ansible/playbooks/deploy_paperclip_stack.yml index 2ee1b5d..87d6f09 100644 --- a/ansible/playbooks/deploy_paperclip_stack.yml +++ b/ansible/playbooks/deploy_paperclip_stack.yml @@ -1,5 +1,5 @@ --- -- name: Deploy Paperclip stack with nginx and certbot +- name: Deploy Paperclip stack (behind the shared nginx-proxy) hosts: illuminations become: true vars_files: @@ -8,77 +8,75 @@ roles: - docker-host + - nginx-proxy # must come before paperclip-docker (it publishes its vhost through it) - paperclip-docker post_tasks: - - name: Verify Paperclip stack is running - community.docker.docker_container_info: - name: "{{ item }}" + # CLI-based on purpose: community.docker.docker_container_info needs the + # Docker SDK for Python, which the paperclip-docker role no longer installs. + - name: Verify stack containers are running + ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ item }}" register: container_status loop: + - paperclip-db - paperclip - - paperclip-nginx - - paperclip-certbot - changed_when: false - - - name: Fail if any container is not running - fail: - msg: "Container {{ item.item }} is not running or does not exist" - when: not item.exists or item.container.State.Running == false - loop: "{{ container_status.results }}" + - nginx + - certbot changed_when: false + failed_when: container_status.rc != 0 or container_status.stdout | trim != 'true' + loop_control: + label: "{{ item }}" - name: Verify Docker is available - command: docker --version + ansible.builtin.command: docker --version register: docker_version changed_when: false - name: Show Docker version - debug: + ansible.builtin.debug: msg: "Docker version: {{ docker_version.stdout }}" - name: Verify docker-compose plugin is available - command: docker compose version + ansible.builtin.command: docker compose version register: compose_version changed_when: false ignore_errors: true - name: Show docker-compose version - debug: + ansible.builtin.debug: msg: "Docker Compose version: {{ compose_version.stdout | default('docker-compose plugin not found') }}" - name: Display deployment summary - debug: + ansible.builtin.debug: msg: | Paperclip stack deployed successfully! - + Access points: - HTTPS: https://{{ paperclip_domain }} - HTTP (redirects to HTTPS): http://{{ paperclip_domain }} - + Services: - Paperclip: {{ paperclip_host }}:{{ paperclip_port }} - - nginx (reverse proxy): 0.0.0.0:443 / 0.0.0.0:80 + - nginx (shared reverse proxy): 0.0.0.0:443 / 0.0.0.0:80, config in {{ nginx_proxy_conf_dir }} - certbot: Running certificate renewal checks - + Data location: - {{ paperclip_base_path }}/data - + Certificates: - - {{ paperclip_certs_dir }}/live/{{ paperclip_domain }}/ - + - {{ nginx_proxy_certs_dir }}/live/{{ paperclip_domain }}/ + vLLM endpoint: - {{ vllm_endpoint }} - + OpenCode configuration: - Config: {{ paperclip_base_path }}/.config/opencode/opencode.jsonc - Model: Gemma-4-26B (AWQ) - Endpoint: {{ vllm_endpoint }}/v1 - + Next steps: 1. Verify DNS resolution for {{ paperclip_domain }} 2. Visit https://{{ paperclip_domain }} to access Paperclip 3. OpenCode should auto-initialize with vLLM at {{ vllm_endpoint }} - 4. Set up Hermes Gateway on Mac Studio + 4. Run day1_deploy_hermes_docker.yml (publishes its dashboard through the same nginx) 5. Configure agents and skills - \ No newline at end of file diff --git a/ansible/roles/hermes_docker/defaults/main.yml b/ansible/roles/hermes_docker/defaults/main.yml new file mode 100644 index 0000000..05a6e00 --- /dev/null +++ b/ansible/roles/hermes_docker/defaults/main.yml @@ -0,0 +1,91 @@ +--- +hermes_image: nousresearch/hermes-agent:latest +# missing = only pull if absent (no surprise upgrades on redeploy). +# Set to "always" when you actually want to upgrade the image. +hermes_pull_policy: missing + +hermes_container_name: hermes +hermes_project_dir: /opt/hermes +hermes_data_dir: "{{ hermes_project_dir }}/data" +# The container's hermes user. The image's start-up hook chowns the data +# volume to it; the role only needs the UID to stage files it can read. +hermes_uid: 10000 +# config.yaml, .env and sessions live here and can hold API keys, so keep it +# owner-only. Passed to the container as HERMES_HOME_MODE (see compose). +hermes_data_dir_mode: "0700" +hermes_secrets_env_file: "{{ hermes_project_dir }}/hermes-secrets.env" + +# Same network namespace as Paperclip and nginx +hermes_network_mode: host +hermes_memory_limit: 8G +hermes_cpu_limit: "4.0" +# Playwright/Chromium needs shared memory (upstream troubleshooting note) +hermes_shm_size: 1g + +# --- Gateway / API server (loopback only; Paperclip reaches it locally) --- +hermes_gateway_port: 8642 +hermes_api_server_enabled: true +hermes_api_server_host: 127.0.0.1 +# Leave empty to run keyless on loopback. If you set one, Paperclip (or any +# other client) must send it as a bearer token. +# e.g. hermes_api_server_key: "{{ vault_hermes_api_server_key }}" +hermes_api_server_key: "" + +# --- Dashboard (bound to loopback, published through nginx) --- +hermes_dashboard_enabled: true +hermes_dashboard_host: 127.0.0.1 +hermes_dashboard_port: 9119 +hermes_domain: haunted-mansion.wdw-labs.cloud +# Publish the dashboard through the nginx-proxy role (cert + vhost + reload). +# Requires day1_deploy_nginx_proxy.yml to have run first. +hermes_publish_vhost: true +hermes_public_url: "https://{{ hermes_domain }}" +hermes_dashboard_username: admin + +# Vault variables this role requires (see README block at bottom of file) +hermes_required_vault_vars: + - vault_hermes_dashboard_password_hash + - vault_hermes_dashboard_secret + +# --- Preflight --- +hermes_preflight_ports: + - "{{ hermes_gateway_port }}" + - "{{ hermes_dashboard_port }}" +hermes_wait_timeout: 120 + +# --- config.yaml overrides (deep-merged, only the keys listed here) --- +# nginx runs with host networking, so it reaches the dashboard from loopback. +# Model settings are NOT managed by default (your wizard-written config stays +# untouched). To manage them, add a model block, for example: +# model: +# provider: custom +# model: +# base_url: http://10.1.71.130:8000/v1 +# api_key: "{{ vault_vllm_api_key }}" +hermes_config_overrides: + dashboard: + trusted_proxies: + - "127.0.0.1" + +# Main model (written to config.yaml under model:). Leave empty to manage it +# by hand with `hermes model` or the dashboard. Example: +# hermes_model: +# provider: custom +# default: +# base_url: http://fantasmic.wdw-labs.cloud:8000/v1 +# api_key: "{{ vault_fantasmic_api_key }}" +# # context_length: 65536 # only if Hermes complains about the context window +# The api_key lands in config.yaml in plain text (owner-only, see hermes_data_dir_mode). +hermes_model: {} + +# What the merge task actually applies: base overrides + the model block. +hermes_effective_overrides: >- + {{ hermes_config_overrides + | combine({'model': hermes_model} if (hermes_model | length > 0) else {}, recursive=True) }} + +# Required vault vars: +# vault_hermes_dashboard_password_hash - output of hash_password() (see below) +# vault_hermes_dashboard_secret - openssl rand -base64 32 +# Generate the hash with: +# docker exec -it -w /opt/hermes hermes /opt/hermes/.venv/bin/python -c \ +# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))" diff --git a/ansible/roles/hermes_docker/files/merge_config.py b/ansible/roles/hermes_docker/files/merge_config.py new file mode 100644 index 0000000..1268497 --- /dev/null +++ b/ansible/roles/hermes_docker/files/merge_config.py @@ -0,0 +1,40 @@ +"""Deep-merge overrides (a JSON file, path in argv[1]) into config.yaml. + +Run inside the hermes container as the hermes user. Only keys present in the +overrides are touched. Prints CHANGED when the file was rewritten. +""" +import json +import os +import sys + +import yaml + +PATH = "/opt/data/config.yaml" +with open(sys.argv[1]) as fh: + overrides = json.load(fh) + +if os.path.exists(PATH): + with open(PATH) as fh: + current = yaml.safe_load(fh) or {} +else: + current = {} + + +def merge(dst, src): + changed = False + for key, val in src.items(): + if isinstance(val, dict) and isinstance(dst.get(key), dict): + changed |= merge(dst[key], val) + elif dst.get(key) != val: + dst[key] = val + changed = True + return changed + + +if merge(current, overrides): + with open(PATH, "w") as fh: + yaml.safe_dump(current, fh, sort_keys=False) + print("CHANGED") +else: + print("UNCHANGED") +sys.exit(0) diff --git a/ansible/roles/hermes_docker/handlers/main.yml b/ansible/roles/hermes_docker/handlers/main.yml new file mode 100644 index 0000000..9ef72a5 --- /dev/null +++ b/ansible/roles/hermes_docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +# Config.yaml is read at start, so a plain restart is enough. +# Compose/env changes are handled by docker compose itself (it recreates). +- name: Restart hermes + ansible.builtin.command: "docker restart {{ hermes_container_name }}" diff --git a/ansible/roles/hermes_docker/tasks/main.yml b/ansible/roles/hermes_docker/tasks/main.yml new file mode 100644 index 0000000..b5f4cc6 --- /dev/null +++ b/ansible/roles/hermes_docker/tasks/main.yml @@ -0,0 +1,148 @@ +--- +- name: Preflight | Required vault variables are defined + ansible.builtin.assert: + that: "{{ item }} is defined and ({{ item }} | length) > 0" + fail_msg: "{{ item }} is not defined. Add it to your ansible vault." + quiet: true + loop: "{{ hermes_required_vault_vars }}" + tags: [hermes, preflight] + +- name: Preflight | Confirm docker CLI is available + ansible.builtin.command: docker --version + changed_when: false + tags: [hermes, preflight] + +- name: Preflight | Get existing container state + ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ hermes_container_name }}" + register: hermes_existing + failed_when: false + changed_when: false + tags: [hermes, preflight] + +# Only enforce free ports when our container is not already running, +# so redeploys stay idempotent. +- name: Preflight | Ensure required ports are free + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ item }}" + state: stopped + timeout: 3 + loop: "{{ hermes_preflight_ports }}" + when: hermes_existing.stdout | default('') | trim != 'true' + tags: [hermes, preflight] + +- name: Create project directory + ansible.builtin.file: + path: "{{ hermes_project_dir }}" + state: directory + mode: "0755" + tags: [hermes] + +# No owner here: the image's start-up hook chowns the volume to the hermes user. +- name: Create data directory (owner-only) + ansible.builtin.file: + path: "{{ hermes_data_dir }}" + state: directory + mode: "{{ hermes_data_dir_mode }}" + tags: [hermes] + +# Compose reads this as root on the host; the container never sees the file. +- name: Template secrets env file + ansible.builtin.template: + src: hermes-secrets.env.j2 + dest: "{{ hermes_secrets_env_file }}" + owner: root + group: root + mode: "0600" + no_log: true + tags: [hermes] + +- name: Template docker compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ hermes_project_dir }}/docker-compose.yml" + mode: "0644" + tags: [hermes] + +- name: Deploy hermes + community.docker.docker_compose_v2: + project_src: "{{ hermes_project_dir }}" + pull: "{{ hermes_pull_policy }}" + state: present + tags: [hermes] + +- name: Verify gateway health + ansible.builtin.uri: + url: "http://127.0.0.1:{{ hermes_gateway_port }}/health" + status_code: 200 + register: hermes_health + until: hermes_health.status == 200 + retries: "{{ (hermes_wait_timeout | int) // 5 }}" + delay: 5 + when: hermes_api_server_enabled | bool + tags: [hermes, verify] + +- name: Wait for dashboard port + ansible.builtin.wait_for: + host: "{{ hermes_dashboard_host }}" + port: "{{ hermes_dashboard_port }}" + timeout: "{{ hermes_wait_timeout }}" + when: hermes_dashboard_enabled | bool + tags: [hermes, verify] + +# Overrides can contain secrets (the model api_key), so they are staged in a +# 0600 file owned by the container user instead of travelling in a command +# line (visible in `ps`), and removed again afterwards. The merge script runs +# inside the container with the image's own venv (PyYAML is available there), +# as the hermes user, so config.yaml stays owned by it. +- name: Apply config.yaml overrides + when: hermes_effective_overrides | length > 0 + tags: [hermes, config] + block: + - name: Stage overrides file + ansible.builtin.copy: + content: "{{ hermes_effective_overrides | to_json }}" + dest: "{{ hermes_data_dir }}/.ansible-overrides.json" + owner: "{{ hermes_uid }}" + group: "{{ hermes_uid }}" + mode: "0600" + changed_when: false # transient file; only the merge result should count as a change + no_log: true + + - name: Merge into config.yaml + ansible.builtin.command: + argv: + - docker + - exec + - -i + - --user + - hermes + - "{{ hermes_container_name }}" + - /opt/hermes/.venv/bin/python + - "-" + - /opt/data/.ansible-overrides.json + stdin: "{{ lookup('ansible.builtin.file', 'merge_config.py') }}" + register: hermes_config_merge + changed_when: "'CHANGED' in hermes_config_merge.stdout and 'UNCHANGED' not in hermes_config_merge.stdout" + notify: Restart hermes + + always: + - name: Remove staged overrides file + ansible.builtin.file: + path: "{{ hermes_data_dir }}/.ansible-overrides.json" + state: absent + changed_when: false + +- name: Publish dashboard through nginx-proxy + ansible.builtin.include_role: + name: nginx-proxy + tasks_from: add_vhost + vars: + vhost_name: hermes + vhost_domain: "{{ hermes_domain }}" + vhost_upstream_port: "{{ hermes_dashboard_port }}" + vhost_proxy_timeout: 3600s # long-lived dashboard WebSockets + when: + - hermes_publish_vhost | bool + - hermes_dashboard_enabled | bool + tags: [hermes, nginx] diff --git a/ansible/roles/hermes_docker/templates/docker-compose.yml.j2 b/ansible/roles/hermes_docker/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..a643a3f --- /dev/null +++ b/ansible/roles/hermes_docker/templates/docker-compose.yml.j2 @@ -0,0 +1,31 @@ +# Managed by Ansible (hermes_docker role) +services: + hermes: + image: {{ hermes_image }} + container_name: {{ hermes_container_name }} + restart: unless-stopped + network_mode: {{ hermes_network_mode }} + command: gateway run + shm_size: "{{ hermes_shm_size }}" + env_file: + - {{ hermes_secrets_env_file }} + environment: + HERMES_HOME_MODE: "{{ hermes_data_dir_mode }}" +{% if hermes_dashboard_enabled | bool %} + HERMES_DASHBOARD: "1" + HERMES_DASHBOARD_HOST: "{{ hermes_dashboard_host }}" + HERMES_DASHBOARD_PORT: "{{ hermes_dashboard_port }}" + HERMES_DASHBOARD_PUBLIC_URL: "{{ hermes_public_url }}" +{% endif %} +{% if hermes_api_server_enabled | bool %} + API_SERVER_ENABLED: "true" + API_SERVER_HOST: "{{ hermes_api_server_host }}" + API_SERVER_PORT: "{{ hermes_gateway_port }}" +{% endif %} + volumes: + - {{ hermes_data_dir }}:/opt/data + deploy: + resources: + limits: + memory: {{ hermes_memory_limit }} + cpus: "{{ hermes_cpu_limit }}" diff --git a/ansible/roles/hermes_docker/templates/hermes-secrets.env.j2 b/ansible/roles/hermes_docker/templates/hermes-secrets.env.j2 new file mode 100644 index 0000000..5d07d74 --- /dev/null +++ b/ansible/roles/hermes_docker/templates/hermes-secrets.env.j2 @@ -0,0 +1,8 @@ +# Managed by Ansible (hermes_docker role) - do not edit +# Single quotes keep compose from interpolating '$' inside hashes/secrets. +HERMES_DASHBOARD_BASIC_AUTH_USERNAME={{ hermes_dashboard_username }} +HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH='{{ vault_hermes_dashboard_password_hash }}' +HERMES_DASHBOARD_BASIC_AUTH_SECRET='{{ vault_hermes_dashboard_secret }}' +{% if hermes_api_server_key | length > 0 %} +API_SERVER_KEY='{{ hermes_api_server_key }}' +{% endif %} diff --git a/ansible/roles/hermes_docker_backup/defaults/main.yml b/ansible/roles/hermes_docker_backup/defaults/main.yml new file mode 100644 index 0000000..de72ea5 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/defaults/main.yml @@ -0,0 +1,66 @@ +--- +hermes_image: nousresearch/hermes-agent:latest +# missing = only pull if absent (no surprise upgrades on redeploy). +# Set to "always" when you actually want to upgrade the image. +hermes_pull_policy: missing + +hermes_container_name: hermes +hermes_project_dir: /opt/hermes +hermes_data_dir: "{{ hermes_project_dir }}/data" +hermes_secrets_env_file: "{{ hermes_project_dir }}/hermes-secrets.env" + +# Same network namespace as Paperclip and nginx +hermes_network_mode: host +hermes_memory_limit: 8G +hermes_cpu_limit: "4.0" +# Playwright/Chromium needs shared memory (upstream troubleshooting note) +hermes_shm_size: 1g + +# --- Gateway / API server (loopback only; Paperclip reaches it locally) --- +hermes_gateway_port: 8642 +hermes_api_server_enabled: true +hermes_api_server_host: 127.0.0.1 +# Leave empty to run keyless on loopback. If you set one, Paperclip (or any +# other client) must send it as a bearer token. +# e.g. hermes_api_server_key: "{{ vault_hermes_api_server_key }}" +hermes_api_server_key: "{{ vault_hermes_api_server_key }}" + +# --- Dashboard (bound to loopback, published through nginx) --- +hermes_dashboard_enabled: true +hermes_dashboard_host: 127.0.0.1 +hermes_dashboard_port: 9119 +hermes_domain: haunted-mansion.wdw-labs.cloud +hermes_public_url: "https://{{ hermes_domain }}" +hermes_dashboard_username: admin + +# Vault variables this role requires (see README block at bottom of file) +hermes_required_vault_vars: + - vault_hermes_dashboard_password_hash + - vault_hermes_dashboard_secret + +# --- Preflight --- +hermes_preflight_ports: + - "{{ hermes_gateway_port }}" + - "{{ hermes_dashboard_port }}" +hermes_wait_timeout: 120 + +# --- config.yaml overrides (deep-merged, only the keys listed here) --- +# nginx runs with host networking, so it reaches the dashboard from loopback. +# Model settings are NOT managed by default (your wizard-written config stays +# untouched). To manage them, add a model block, for example: +# model: +# provider: custom +# model: +# base_url: http://10.1.71.130:8000/v1 +# api_key: "{{ vault_vllm_api_key }}" +hermes_config_overrides: + dashboard: + trusted_proxies: + - "127.0.0.1" + +# Required vault vars: +# vault_hermes_dashboard_password_hash - output of hash_password() (see below) +# vault_hermes_dashboard_secret - openssl rand -base64 32 +# Generate the hash with: +# docker exec -it -w /opt/hermes hermes /opt/hermes/.venv/bin/python -c \ +# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))" diff --git a/ansible/roles/hermes_docker_backup/files/merge_config.py b/ansible/roles/hermes_docker_backup/files/merge_config.py new file mode 100644 index 0000000..bdf8b23 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/files/merge_config.py @@ -0,0 +1,39 @@ +"""Deep-merge overrides (JSON in $HERMES_CONFIG_OVERRIDES) into config.yaml. + +Run inside the hermes container as the hermes user. Only keys present in the +overrides are touched. Prints CHANGED when the file was rewritten. +""" +import json +import os +import sys + +import yaml + +PATH = "/opt/data/config.yaml" +overrides = json.loads(os.environ["HERMES_CONFIG_OVERRIDES"]) + +if os.path.exists(PATH): + with open(PATH) as fh: + current = yaml.safe_load(fh) or {} +else: + current = {} + + +def merge(dst, src): + changed = False + for key, val in src.items(): + if isinstance(val, dict) and isinstance(dst.get(key), dict): + changed |= merge(dst[key], val) + elif dst.get(key) != val: + dst[key] = val + changed = True + return changed + + +if merge(current, overrides): + with open(PATH, "w") as fh: + yaml.safe_dump(current, fh, sort_keys=False) + print("CHANGED") +else: + print("UNCHANGED") +sys.exit(0) diff --git a/ansible/roles/hermes_docker_backup/handlers/main.yml b/ansible/roles/hermes_docker_backup/handlers/main.yml new file mode 100644 index 0000000..9ef72a5 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/handlers/main.yml @@ -0,0 +1,5 @@ +--- +# Config.yaml is read at start, so a plain restart is enough. +# Compose/env changes are handled by docker compose itself (it recreates). +- name: Restart hermes + ansible.builtin.command: "docker restart {{ hermes_container_name }}" diff --git a/ansible/roles/hermes_docker_backup/nginx-hermes-vhost.conf.j2 b/ansible/roles/hermes_docker_backup/nginx-hermes-vhost.conf.j2 new file mode 100644 index 0000000..5e4b15a --- /dev/null +++ b/ansible/roles/hermes_docker_backup/nginx-hermes-vhost.conf.j2 @@ -0,0 +1,49 @@ + # ---- Hermes dashboard: paste inside the http { } block of nginx.conf.j2 ---- + # Redirect HTTP to HTTPS + server { + listen 80; + server_name {{ hermes_domain }}; + return 301 https://$server_name$request_uri; + } + + server { + listen 443 ssl http2; + server_name {{ hermes_domain }}; + + ssl_certificate /etc/nginx/certs/live/{{ hermes_domain }}/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/live/{{ hermes_domain }}/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + # No auth_basic here: the dashboard has its own login (basic auth provider). + location / { + proxy_pass http://127.0.0.1:{{ hermes_dashboard_port }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Port $server_port; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_connect_timeout 60s; + proxy_send_timeout 3600s; + proxy_read_timeout 3600s; + + proxy_buffering off; + proxy_request_buffering off; + } + } diff --git a/ansible/roles/hermes_docker_backup/tasks/main.yml b/ansible/roles/hermes_docker_backup/tasks/main.yml new file mode 100644 index 0000000..0e97247 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Preflight | Required vault variables are defined + ansible.builtin.assert: + that: "{{ item }} is defined and ({{ item }} | length) > 0" + fail_msg: "{{ item }} is not defined. Add it to your ansible vault." + quiet: true + loop: "{{ hermes_required_vault_vars }}" + tags: [hermes, preflight] + +- name: Preflight | Confirm docker CLI is available + ansible.builtin.command: docker --version + changed_when: false + tags: [hermes, preflight] + +- name: Preflight | Get existing container state + community.docker.docker_container_info: + name: "{{ hermes_container_name }}" + register: hermes_existing + tags: [hermes, preflight] + +# Only enforce free ports when our container is not already running, +# so redeploys stay idempotent. +- name: Preflight | Ensure required ports are free + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ item }}" + state: stopped + timeout: 3 + loop: "{{ hermes_preflight_ports }}" + when: not (hermes_existing.container.State.Running | default(false)) + tags: [hermes, preflight] + +- name: Create project and data directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ hermes_project_dir }}" + - "{{ hermes_data_dir }}" + tags: [hermes] + +# Compose reads this as root on the host; the container never sees the file. +- name: Template secrets env file + ansible.builtin.template: + src: hermes-secrets.env.j2 + dest: "{{ hermes_secrets_env_file }}" + owner: root + group: root + mode: "0600" + no_log: true + tags: [hermes] + +- name: Template docker compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ hermes_project_dir }}/docker-compose.yml" + mode: "0644" + tags: [hermes] + +- name: Deploy hermes + community.docker.docker_compose_v2: + project_src: "{{ hermes_project_dir }}" + pull: "{{ hermes_pull_policy }}" + state: present + tags: [hermes] + +- name: Verify gateway health + ansible.builtin.uri: + url: "http://127.0.0.1:{{ hermes_gateway_port }}/health" + status_code: 200 + register: hermes_health + until: hermes_health.status == 200 + retries: "{{ (hermes_wait_timeout | int) // 5 }}" + delay: 5 + when: hermes_api_server_enabled | bool + tags: [hermes, verify] + +- name: Wait for dashboard port + ansible.builtin.wait_for: + host: "{{ hermes_dashboard_host }}" + port: "{{ hermes_dashboard_port }}" + timeout: "{{ hermes_wait_timeout }}" + when: hermes_dashboard_enabled | bool + tags: [hermes, verify] + +# Runs inside the container with the image's own venv (PyYAML is available +# there) as the hermes user, so config.yaml stays owned by UID 10000. +- name: Merge config.yaml overrides + ansible.builtin.command: + argv: + - docker + - exec + - -i + - --user + - hermes + - -e + - "HERMES_CONFIG_OVERRIDES={{ hermes_config_overrides | to_json }}" + - "{{ hermes_container_name }}" + - /opt/hermes/.venv/bin/python + - "-" + stdin: "{{ lookup('ansible.builtin.file', 'merge_config.py') }}" + register: hermes_config_merge + changed_when: "'CHANGED' in hermes_config_merge.stdout and 'UNCHANGED' not in hermes_config_merge.stdout" + no_log: true + when: hermes_config_overrides | length > 0 + notify: Restart hermes + tags: [hermes, config] diff --git a/ansible/roles/hermes_docker_backup/templates/docker-compose.yml.j2 b/ansible/roles/hermes_docker_backup/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..4323306 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/templates/docker-compose.yml.j2 @@ -0,0 +1,30 @@ +# Managed by Ansible (hermes_docker role) +services: + hermes: + image: {{ hermes_image }} + container_name: {{ hermes_container_name }} + restart: unless-stopped + network_mode: {{ hermes_network_mode }} + command: gateway run + shm_size: "{{ hermes_shm_size }}" + env_file: + - {{ hermes_secrets_env_file }} + environment: +{% if hermes_dashboard_enabled | bool %} + HERMES_DASHBOARD: "1" + HERMES_DASHBOARD_HOST: "{{ hermes_dashboard_host }}" + HERMES_DASHBOARD_PORT: "{{ hermes_dashboard_port }}" + HERMES_DASHBOARD_PUBLIC_URL: "{{ hermes_public_url }}" +{% endif %} +{% if hermes_api_server_enabled | bool %} + API_SERVER_ENABLED: "true" + API_SERVER_HOST: "{{ hermes_api_server_host }}" + API_SERVER_PORT: "{{ hermes_gateway_port }}" +{% endif %} + volumes: + - {{ hermes_data_dir }}:/opt/data + deploy: + resources: + limits: + memory: {{ hermes_memory_limit }} + cpus: "{{ hermes_cpu_limit }}" diff --git a/ansible/roles/hermes_docker_backup/templates/hermes-secrets.env.j2 b/ansible/roles/hermes_docker_backup/templates/hermes-secrets.env.j2 new file mode 100644 index 0000000..5d07d74 --- /dev/null +++ b/ansible/roles/hermes_docker_backup/templates/hermes-secrets.env.j2 @@ -0,0 +1,8 @@ +# Managed by Ansible (hermes_docker role) - do not edit +# Single quotes keep compose from interpolating '$' inside hashes/secrets. +HERMES_DASHBOARD_BASIC_AUTH_USERNAME={{ hermes_dashboard_username }} +HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH='{{ vault_hermes_dashboard_password_hash }}' +HERMES_DASHBOARD_BASIC_AUTH_SECRET='{{ vault_hermes_dashboard_secret }}' +{% if hermes_api_server_key | length > 0 %} +API_SERVER_KEY='{{ hermes_api_server_key }}' +{% endif %} diff --git a/ansible/roles/nginx-proxy/README.md b/ansible/roles/nginx-proxy/README.md new file mode 100644 index 0000000..6fb9d75 --- /dev/null +++ b/ansible/roles/nginx-proxy/README.md @@ -0,0 +1,28 @@ +# nginx-proxy + +Shared nginx + certbot (Cloudflare DNS challenge) under /opt/nginx. Apps publish +themselves with `tasks_from: add_vhost`; see the header of tasks/add_vhost.yml. + +Layout on the host: + + /opt/nginx/ + docker-compose.yml .env (0600) + conf.d/ 00-global.conf + one .conf per vhost -> /etc/nginx/conf.d + certs/ certbot state (live/, archive/, renewal/) -> /etc/letsencrypt, /etc/nginx/certs + cert-domains/ one empty file per domain (filename = domain) + scripts/ certbot-init.sh + +Required variables: `nginx_proxy_certbot_email`, and `vault_cloudflare_api_token` +(or `nginx_proxy_cloudflare_api_token`). + +Prerequisite (manual): a DNS record for each app domain pointing at this host. + +Staging: set `nginx_proxy_certbot_staging: true` to test the DNS challenge without +using Let's Encrypt rate limits. Staging certs are skipped-as-existing afterwards, so +to move a domain to a real cert: `docker exec certbot certbot delete --cert-name ` +and re-run its play. + +Retire a domain: delete its cert-domains/ marker and conf.d/.conf, reload nginx, +then `docker exec certbot certbot delete --cert-name `. + +Pin nginx older than 1.25.1? Set `nginx_proxy_http2: false` (the template uses `http2 on;`). diff --git a/ansible/roles/nginx-proxy/defaults/main.yml b/ansible/roles/nginx-proxy/defaults/main.yml new file mode 100644 index 0000000..ebaa05b --- /dev/null +++ b/ansible/roles/nginx-proxy/defaults/main.yml @@ -0,0 +1,27 @@ +--- +nginx_proxy_base_path: /opt/nginx +nginx_proxy_certs_dir: "{{ nginx_proxy_base_path }}/certs" +nginx_proxy_conf_dir: "{{ nginx_proxy_base_path }}/conf.d" +nginx_proxy_domains_dir: "{{ nginx_proxy_base_path }}/cert-domains" +nginx_proxy_scripts_dir: "{{ nginx_proxy_base_path }}/scripts" + +nginx_proxy_nginx_container: nginx +nginx_proxy_certbot_container: certbot +nginx_proxy_nginx_image: nginx:latest +nginx_proxy_certbot_image: certbot/dns-cloudflare:latest + +# Required: set these in group_vars/host_vars (token comes from the vault) +nginx_proxy_certbot_email: "" +nginx_proxy_cloudflare_api_token: "{{ vault_cloudflare_api_token }}" + +# true = Let's Encrypt staging (untrusted certs, no rate-limit risk). Use while iterating. +nginx_proxy_certbot_staging: false + +# nginx reloads this often so renewed certs get picked up +nginx_proxy_reload_interval: 6h +nginx_proxy_client_max_body_size: 100M + +# "http2 on;" needs nginx >= 1.25.1 (fine for nginx:latest). Set false if you pin older. +nginx_proxy_http2: true + +nginx_proxy_pull_policy: missing diff --git a/ansible/roles/nginx-proxy/files/certbot-init.sh b/ansible/roles/nginx-proxy/files/certbot-init.sh new file mode 100644 index 0000000..3543795 --- /dev/null +++ b/ansible/roles/nginx-proxy/files/certbot-init.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# Issues certificates (one per domain) via the Cloudflare DNS challenge. +# +# certbot-init.sh -> every domain with a marker file in $DOMAINS_DIR +# certbot-init.sh a.example.com -> only the domains given as arguments +# +# Idempotent: domains that already have a certificate are skipped. +# Renewal is NOT handled here; `certbot renew` covers every existing lineage. +set -eu + +DOMAINS_DIR="${DOMAINS_DIR:-/etc/cert-domains.d}" +EMAIL="${CERTBOT_EMAIL:-}" +TOKEN="${CLOUDFLARE_API_TOKEN:-}" + +STAGING_ARG="" +if [ "${CERTBOT_STAGING:-0}" = "1" ]; then + STAGING_ARG="--server https://acme-staging-v02.api.letsencrypt.org/directory" + echo "NOTE: using the Let's Encrypt STAGING server (certificates will not be trusted)" +fi + +if [ "$#" -gt 0 ]; then + DOMAINS="$*" +elif [ -d "$DOMAINS_DIR" ]; then + DOMAINS=$(ls -1 "$DOMAINS_DIR" | grep -E '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$' || true) +else + DOMAINS="" +fi +DOMAINS=$(printf '%s\n' "$DOMAINS" | tr ', \t' '\n\n\n' | grep -v '^$' | sort -u || true) + +if [ -z "$DOMAINS" ]; then + echo "No domains registered in $DOMAINS_DIR; nothing to do" + exit 0 +fi + +if [ -z "$EMAIL" ]; then + echo "ERROR: CERTBOT_EMAIL not set" + exit 1 +fi +if [ -z "$TOKEN" ]; then + echo "ERROR: CLOUDFLARE_API_TOKEN not set" + exit 1 +fi + +# Temporary credentials file, shared by all requests and always cleaned up +CREDS_FILE=$(mktemp /tmp/cloudflare.XXXXXX) +trap 'rm -f "$CREDS_FILE"' EXIT +chmod 600 "$CREDS_FILE" +printf 'dns_cloudflare_api_token = %s\n' "$TOKEN" > "$CREDS_FILE" + +FAILED="" +ISSUED=0 +SKIPPED=0 + +for DOMAIN in $DOMAINS; do + CERT_PATH="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" + + if [ -f "$CERT_PATH" ]; then + echo "Certificate for $DOMAIN already exists at $CERT_PATH" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo "Requesting certificate for $DOMAIN via Cloudflare DNS challenge..." + + # --cert-name pins the lineage directory to the domain name so the + # live// path nginx expects never gets a -0001 suffix. + # STAGING_ARG is intentionally unquoted (it expands to two words). + # shellcheck disable=SC2086 + if certbot certonly \ + --dns-cloudflare \ + --dns-cloudflare-credentials "$CREDS_FILE" \ + --agree-tos \ + --no-eff-email \ + --email "$EMAIL" \ + --non-interactive \ + --cert-name "$DOMAIN" \ + $STAGING_ARG \ + -d "$DOMAIN" && [ -f "$CERT_PATH" ]; then + echo "Certificate obtained and verified for $DOMAIN at $CERT_PATH" + ISSUED=$((ISSUED + 1)) + else + echo "ERROR: Failed to obtain certificate for $DOMAIN" + FAILED="$FAILED $DOMAIN" + fi +done + +echo "Summary: $ISSUED issued, $SKIPPED already present" + +if [ -n "$FAILED" ]; then + echo "ERROR: certificate request failed for:$FAILED" + exit 1 +fi diff --git a/ansible/roles/nginx-proxy/handlers/main.yml b/ansible/roles/nginx-proxy/handlers/main.yml new file mode 100644 index 0000000..8c4c81d --- /dev/null +++ b/ansible/roles/nginx-proxy/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Reload nginx + ansible.builtin.shell: > + docker exec {{ nginx_proxy_nginx_container }} nginx -t && + docker exec {{ nginx_proxy_nginx_container }} nginx -s reload + changed_when: true diff --git a/ansible/roles/nginx-proxy/tasks/add_vhost.yml b/ansible/roles/nginx-proxy/tasks/add_vhost.yml new file mode 100644 index 0000000..71b6dfc --- /dev/null +++ b/ansible/roles/nginx-proxy/tasks/add_vhost.yml @@ -0,0 +1,118 @@ +--- +# Publish an app through nginx-proxy. Usage: +# +# - ansible.builtin.include_role: +# name: nginx-proxy +# tasks_from: add_vhost +# vars: +# vhost_name: paperclip # required, becomes conf.d/.conf +# vhost_domain: "{{ paperclip_domain }}" # required +# vhost_upstream_port: 3100 # required +# vhost_upstream_host: 127.0.0.1 # optional +# vhost_proxy_timeout: 60s # optional (use 3600s for WebSockets) +# vhost_health_path: /health # optional, proxied without access log +# +# Order: marker -> certificate -> vhost file -> nginx -t -> reload. +# The vhost is only written once the certificate exists, and a vhost that +# fails nginx -t is rolled back, so one bad app cannot take the others down. +- name: Validate vhost parameters + ansible.builtin.assert: + that: + - vhost_name is defined + - vhost_domain is defined + - vhost_upstream_port is defined + fail_msg: "add_vhost needs vhost_name, vhost_domain and vhost_upstream_port" + quiet: true + +- name: Check that the nginx-proxy containers are running + ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ item }}" + register: nginx_proxy_running + failed_when: nginx_proxy_running.rc != 0 or nginx_proxy_running.stdout | trim != 'true' + changed_when: false + loop: + - "{{ nginx_proxy_nginx_container }}" + - "{{ nginx_proxy_certbot_container }}" + loop_control: + label: "{{ item }}" + +- name: "Register cert marker for {{ vhost_domain }}" + ansible.builtin.file: + path: "{{ nginx_proxy_domains_dir }}/{{ vhost_domain }}" + state: touch + modification_time: preserve + access_time: preserve + mode: "0644" + +- name: "Check for existing certificate for {{ vhost_domain }}" + ansible.builtin.stat: + path: "{{ nginx_proxy_certs_dir }}/live/{{ vhost_domain }}/fullchain.pem" + register: nginx_proxy_cert_before + +# Targeted run: only this domain, so another app's failed issuance +# cannot fail this play. +- name: "Issue certificate for {{ vhost_domain }}" + ansible.builtin.command: >- + docker exec {{ nginx_proxy_certbot_container }} + /scripts/certbot-init.sh {{ vhost_domain }} + register: nginx_proxy_cert_issue + changed_when: "'Certificate obtained' in nginx_proxy_cert_issue.stdout" + when: not nginx_proxy_cert_before.stat.exists + +- name: "Confirm certificate exists for {{ vhost_domain }}" + ansible.builtin.stat: + path: "{{ nginx_proxy_certs_dir }}/live/{{ vhost_domain }}/fullchain.pem" + register: nginx_proxy_cert_after + failed_when: not nginx_proxy_cert_after.stat.exists + +- name: "Write {{ vhost_name }} vhost, test, and reload" + block: + - name: "Template conf.d/{{ vhost_name }}.conf" + ansible.builtin.template: + src: vhost.conf.j2 + dest: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf" + mode: "0644" + backup: true + register: nginx_proxy_vhost_file + + - name: Test nginx config + ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -t" + changed_when: false + + - name: Reload nginx + ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -s reload" + when: nginx_proxy_vhost_file.changed + + - name: Remove superseded backup + ansible.builtin.file: + path: "{{ nginx_proxy_vhost_file.backup_file }}" + state: absent + when: nginx_proxy_vhost_file.backup_file is defined + + rescue: + - name: Restore previous vhost + ansible.builtin.copy: + src: "{{ nginx_proxy_vhost_file.backup_file }}" + dest: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf" + remote_src: true + mode: "0644" + when: nginx_proxy_vhost_file.backup_file | default('') | length > 0 + + - name: Remove backup after restoring it + ansible.builtin.file: + path: "{{ nginx_proxy_vhost_file.backup_file }}" + state: absent + when: nginx_proxy_vhost_file.backup_file | default('') | length > 0 + + - name: Remove the new vhost (there was no previous one) + ansible.builtin.file: + path: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf" + state: absent + when: + - nginx_proxy_vhost_file is defined + - nginx_proxy_vhost_file.backup_file | default('') | length == 0 + + - name: Fail with context + ansible.builtin.fail: + msg: >- + nginx rejected the {{ vhost_name }} vhost; it was rolled back and + the running config is unchanged. diff --git a/ansible/roles/nginx-proxy/tasks/main.yml b/ansible/roles/nginx-proxy/tasks/main.yml new file mode 100644 index 0000000..f98abde --- /dev/null +++ b/ansible/roles/nginx-proxy/tasks/main.yml @@ -0,0 +1,70 @@ +--- +- name: Assert required variables + ansible.builtin.assert: + that: + - nginx_proxy_certbot_email | length > 0 + - nginx_proxy_cloudflare_api_token | length > 0 + fail_msg: >- + Set nginx_proxy_certbot_email, and make sure vault_cloudflare_api_token + (or nginx_proxy_cloudflare_api_token) is defined. + quiet: true + tags: [nginx-proxy, preflight] + +- name: Create directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ nginx_proxy_base_path }}" + - "{{ nginx_proxy_certs_dir }}" + - "{{ nginx_proxy_conf_dir }}" + - "{{ nginx_proxy_domains_dir }}" + - "{{ nginx_proxy_scripts_dir }}" + tags: [nginx-proxy] + +- name: Install certbot script + ansible.builtin.copy: + src: certbot-init.sh + dest: "{{ nginx_proxy_scripts_dir }}/certbot-init.sh" + mode: "0755" + tags: [nginx-proxy] + +- name: Template global nginx settings + ansible.builtin.template: + src: 00-global.conf.j2 + dest: "{{ nginx_proxy_conf_dir }}/00-global.conf" + mode: "0644" + notify: Reload nginx + tags: [nginx-proxy] + +- name: Template compose env file + ansible.builtin.template: + src: env.j2 + dest: "{{ nginx_proxy_base_path }}/.env" + mode: "0600" + no_log: true + tags: [nginx-proxy] + +- name: Template compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ nginx_proxy_base_path }}/docker-compose.yml" + mode: "0644" + tags: [nginx-proxy] + +- name: Deploy nginx and certbot + community.docker.docker_compose_v2: + project_src: "{{ nginx_proxy_base_path }}" + pull: "{{ nginx_proxy_pull_policy }}" + state: present + tags: [nginx-proxy] + +- name: Verify nginx config is valid and the container is serving + ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -t" + register: nginx_proxy_test + until: nginx_proxy_test.rc == 0 + retries: 6 + delay: 5 + changed_when: false + tags: [nginx-proxy, verify] diff --git a/ansible/roles/nginx-proxy/templates/00-global.conf.j2 b/ansible/roles/nginx-proxy/templates/00-global.conf.j2 new file mode 100644 index 0000000..daf7e96 --- /dev/null +++ b/ansible/roles/nginx-proxy/templates/00-global.conf.j2 @@ -0,0 +1,31 @@ +# Managed by Ansible (nginx-proxy role) +# Included inside the stock nginx.conf http { } block, so these are http-level. +server_tokens off; +client_max_body_size {{ nginx_proxy_client_max_body_size }}; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers HIGH:!aNULL:!MD5; +ssl_prefer_server_ciphers on; +ssl_session_cache shared:SSL:10m; +ssl_session_timeout 10m; + +# Catch-all: unknown Host headers get nothing, and unknown SNI is rejected +# before any certificate is presented. +server { + listen 80 default_server; + server_name _; + access_log off; + + location = /nginx-health { + return 200 "ok\n"; + } + location / { + return 444; + } +} + +server { + listen 443 ssl default_server; + server_name _; + ssl_reject_handshake on; +} diff --git a/ansible/roles/nginx-proxy/templates/docker-compose.yml.j2 b/ansible/roles/nginx-proxy/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..ba4413a --- /dev/null +++ b/ansible/roles/nginx-proxy/templates/docker-compose.yml.j2 @@ -0,0 +1,46 @@ +# Managed by Ansible (nginx-proxy role) +services: + certbot: + image: {{ nginx_proxy_certbot_image }} + container_name: {{ nginx_proxy_certbot_container }} + volumes: + - {{ nginx_proxy_certs_dir }}:/etc/letsencrypt + - {{ nginx_proxy_scripts_dir }}:/scripts:ro + - {{ nginx_proxy_domains_dir }}:/etc/cert-domains.d:ro + environment: + CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + CERTBOT_STAGING: "{{ '1' if nginx_proxy_certbot_staging | bool else '0' }}" + entrypoint: /bin/sh + command: + - -c + - | + while true; do + /scripts/certbot-init.sh || echo "issuance failed; will retry next cycle" + certbot renew --quiet + sleep 12h + done + restart: unless-stopped + + nginx: + image: {{ nginx_proxy_nginx_image }} + container_name: {{ nginx_proxy_nginx_container }} + network_mode: host + volumes: + # Directory mounts (not single files) so Ansible's atomic file + # replacement is visible inside the container. + - {{ nginx_proxy_conf_dir }}:/etc/nginx/conf.d:ro + - {{ nginx_proxy_certs_dir }}:/etc/nginx/certs:ro + command: + - /bin/sh + - -c + - | + ( while :; do sleep {{ nginx_proxy_reload_interval }}; nginx -s reload; done ) & + exec nginx -g 'daemon off;' + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/nginx-health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/ansible/roles/nginx-proxy/templates/env.j2 b/ansible/roles/nginx-proxy/templates/env.j2 new file mode 100644 index 0000000..0e2cf54 --- /dev/null +++ b/ansible/roles/nginx-proxy/templates/env.j2 @@ -0,0 +1,3 @@ +# Managed by Ansible (nginx-proxy role) +CLOUDFLARE_API_TOKEN={{ nginx_proxy_cloudflare_api_token }} +CERTBOT_EMAIL={{ nginx_proxy_certbot_email }} diff --git a/ansible/roles/nginx-proxy/templates/vhost.conf.j2 b/ansible/roles/nginx-proxy/templates/vhost.conf.j2 new file mode 100644 index 0000000..9816e1e --- /dev/null +++ b/ansible/roles/nginx-proxy/templates/vhost.conf.j2 @@ -0,0 +1,51 @@ +# Managed by Ansible (nginx-proxy role) - vhost: {{ vhost_name }} +server { + listen 80; + server_name {{ vhost_domain }}; + return 301 https://{{ vhost_domain }}$request_uri; +} + +server { + listen 443 ssl; +{% if nginx_proxy_http2 | bool %} + http2 on; +{% endif %} + server_name {{ vhost_domain }}; + + ssl_certificate /etc/nginx/certs/live/{{ vhost_domain }}/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/live/{{ vhost_domain }}/privkey.pem; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + proxy_pass http://{{ vhost_upstream_host | default('127.0.0.1') }}:{{ vhost_upstream_port }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_connect_timeout 60s; + proxy_send_timeout {{ vhost_proxy_timeout | default('60s') }}; + proxy_read_timeout {{ vhost_proxy_timeout | default('60s') }}; + + proxy_buffering off; + proxy_request_buffering off; + } +{% if vhost_health_path | default('') | length > 0 %} + + location {{ vhost_health_path }} { + proxy_pass http://{{ vhost_upstream_host | default('127.0.0.1') }}:{{ vhost_upstream_port }}{{ vhost_health_path }}; + access_log off; + } +{% endif %} +} diff --git a/ansible/roles/paperclip-docker/defaults/main.yml b/ansible/roles/paperclip-docker/defaults/main.yml index 236f3c4..5097bd7 100644 --- a/ansible/roles/paperclip-docker/defaults/main.yml +++ b/ansible/roles/paperclip-docker/defaults/main.yml @@ -2,31 +2,30 @@ # Paperclip deployment paths and configuration paperclip_base_path: /opt/paperclip paperclip_data_dir: "{{ paperclip_base_path }}/data" -paperclip_certs_dir: "{{ paperclip_base_path }}/certs" paperclip_port: 3100 -paperclip_domain: muppet-vision.wdw-labs.cloud -paperclip_host: muppet-vision.wdw-labs.cloud +paperclip_domain: cranium-command.wdw-labs.cloud +paperclip_host: cranium-command.wdw-labs.cloud +# paperclip_listen_host: 127.0.0.1 # Bootstrap local deployment +paperclip_listen_host: 0.0.0.0 # Switch to this host after CEO is functional on local model # Paperclip allowed hostnames (space or comma-separated) # Includes: domain name, localhost (for internal/local access), and client IP paperclip_allowed_hostnames: "{{ paperclip_domain }} localhost 192.168.10.50" # Deployment mode: authenticated (requires login) or local_trusted (no auth, trusted network) -paperclip_deployment_mode: "authenticated" +# paperclip_deployment_mode: "local_trusted" # Bootstrap local deployment +paperclip_deployment_mode: "authenticated" # Switch to this model after CEO is functional on local model -# Reverse proxy configuration -nginx_port: 443 -nginx_http_port: 80 +# TLS / reverse proxy are handled by the nginx-proxy role (/opt/nginx). +# Set nginx_proxy_certbot_email in host_vars; run day1_deploy_nginx_proxy.yml first. -# HTTPS/SSL configuration -certbot_email: ryan.blundon@protonmail.com -certbot_provider: cloudflare - -# Inference endpoint (vLLM or remote OpenAI-compatible) -vllm_endpoint: "http://127.0.0.1:8000" +# Inference endpoints (vLLM or remote OpenAI-compatible) +illuminations_endpoint: "http://illuminations:8000" fantasmic_endpoint: "http://fantasmic.wdw-labs.cloud:8000" -wishes_endpoint: "http://fantasmic.wdw-labs.cloud:8000" +wishes_endpoint: "http://wishes.wdw-labs.cloud:8000" -# NOTE: Authentication secrets (BETTER_AUTH_SECRET, PAPERCLIP_AGENT_JWT_SECRET) -# are auto-generated during deployment and stored in .env (mode 0600) -# No need to specify them here — they're created with: openssl rand -hex 24 +# NOTE: Secrets come from the ansible vault and are written to .env (mode 0600): +# vault_better_auth_secret, vault_paperclip_agent_jwt_secret, +# vault_illuminations_api_key, vault_fantasmic_api_key +# Everything in .env is passed into the Paperclip container (env_file), so only +# put values here that Paperclip itself should be able to read. diff --git a/ansible/roles/paperclip-docker/handlers/main.yml b/ansible/roles/paperclip-docker/handlers/main.yml index cb533cb..6360f2c 100644 --- a/ansible/roles/paperclip-docker/handlers/main.yml +++ b/ansible/roles/paperclip-docker/handlers/main.yml @@ -1,19 +1,12 @@ --- +# Recreate (not just restart) so .env / env_file changes are actually applied. +# Skipped when the deploy task already created or recreated containers this run. - name: Restart paperclip stack - shell: | - cd {{ paperclip_base_path }} - /usr/local/bin/docker-compose restart - become: yes - -- name: Reload nginx - shell: | - docker exec paperclip-nginx nginx -s reload - become: yes - ignore_errors: yes - -- name: Reload certbot - shell: | - docker restart paperclip-certbot - become: yes - ignore_errors: yes - \ No newline at end of file + community.docker.docker_compose_v2: + project_src: "{{ paperclip_base_path }}" + services: + - paperclip + state: present + recreate: always + become: true + when: not (paperclip_deploy.changed | default(false)) diff --git a/ansible/roles/paperclip-docker/tasks/main.yml b/ansible/roles/paperclip-docker/tasks/main.yml index 20c2e8b..d2e3a93 100644 --- a/ansible/roles/paperclip-docker/tasks/main.yml +++ b/ansible/roles/paperclip-docker/tasks/main.yml @@ -1,27 +1,6 @@ --- -- name: Install Docker SDK for Python - apt: - name: python3-docker - state: present - become: true - -- name: Download latest docker-compose from GitHub - shell: | - curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose - become: true - -- name: Verify docker-compose installation - shell: /usr/local/bin/docker-compose --version - register: compose_version - become: true - -- name: Show docker-compose version - debug: - msg: "{{ compose_version.stdout }}" - - name: Create paperclip directories - file: + ansible.builtin.file: path: "{{ item }}" state: directory mode: '0755' @@ -29,97 +8,77 @@ group: root loop: - "{{ paperclip_base_path }}" - - "{{ paperclip_data_dir }}" - - "{{ paperclip_certs_dir }}" + # The postgres image chowns its own data dir on start; do not chown it here. - "{{ paperclip_base_path }}/pgdata" become: true + tags: [paperclip, files] -- name: Create OpenCode config directory - file: - path: "{{ paperclip_base_path }}/.config/opencode" +# Container runs as UID 1000. Deliberately NOT recursive: recurse + mode would +# chmod every file the app writes under these paths on each run. A fresh +# deploy has no root-owned content to repair; if you ever need to, run a +# one-off `chown -R 1000:1000 ` by hand. +- name: Create container-owned directories (UID 1000) + ansible.builtin.file: + path: "{{ item }}" state: directory mode: '0755' owner: 1000 group: 1000 + loop: + - "{{ paperclip_data_dir }}" + - "{{ paperclip_base_path }}/.config/opencode" + - "{{ paperclip_base_path }}/.local/share/opencode" become: true + tags: [paperclip, files] -- name: Create OpenCode .local/share directory - file: - path: "{{ paperclip_base_path }}/.local/share/opencode" - state: directory - mode: '0755' - owner: 1000 - group: 1000 - become: yes - -- name: Deploy OpenCode auth.json with vLLM credentials - template: - src: opencode-auth.json.j2 - dest: "{{ paperclip_base_path }}/.local/share/opencode/auth.json" - mode: '0600' - owner: 1000 - group: 1000 - become: true - no_log: true - -- name: Fix directory ownership for container user (UID 1000) - shell: | - chown -R 1000:1000 {{ paperclip_data_dir }} - chown -R 1000:1000 {{ paperclip_base_path }}/pgdata - chown -R 1000:1000 {{ paperclip_base_path }}/.config - become: true +# - name: Deploy OpenCode auth.json with vLLM credentials +# template: +# src: opencode-auth.json.j2 +# dest: "{{ paperclip_base_path }}/.local/share/opencode/auth.json" +# mode: '0600' +# owner: 1000 +# group: 1000 +# become: true +# no_log: true +# This file is both the compose interpolation source and the container's +# env_file, so everything here is visible inside the Paperclip container. +# Infrastructure secrets (Cloudflare token, certbot email) live in +# /opt/nginx/.env instead. - name: Write secrets and configuration to .env - copy: + ansible.builtin.copy: content: | # Authentication secrets (from vault) BETTER_AUTH_SECRET={{ vault_better_auth_secret }} PAPERCLIP_AGENT_JWT_SECRET={{ vault_paperclip_agent_jwt_secret }} - - # Infrastructure configuration - CLOUDFLARE_API_TOKEN={{ vault_cloudflare_api_token }} - CERTBOT_EMAIL={{ certbot_email }} - PAPERCLIP_DOMAIN={{ paperclip_domain }} - VLLM_ENDPOINT={{ vllm_endpoint }} - VLLM_API_KEY={{ vault_vllm_api_key }} + + # Inference endpoints + ILLUMINATIONS_ENDPOINT={{ illuminations_endpoint }} + ILLUMINATIONS_API_KEY={{ vault_illuminations_api_key }} + FANTASMIC_ENDPOINT={{ fantasmic_endpoint }} + FANTASMIC_API_KEY={{ vault_fantasmic_api_key }} dest: "{{ paperclip_base_path }}/.env" mode: '0600' owner: root group: root become: true no_log: true + notify: Restart paperclip stack + tags: [paperclip, files] +# No notify: the deploy task below recreates whatever the compose file changed. - name: Template docker-compose.yml - template: + ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ paperclip_base_path }}/docker-compose.yml" mode: '0644' owner: root group: root become: true - notify: Restart paperclip stack - -- name: Template nginx config - template: - src: nginx.conf.j2 - dest: "{{ paperclip_base_path }}/nginx.conf" - mode: '0644' - owner: root - group: root - become: true - notify: Reload nginx - -- name: Template certbot init script - template: - src: certbot-init.sh.j2 - dest: "{{ paperclip_base_path }}/certbot-init.sh" - mode: '0755' - owner: root - group: root - become: true + tags: [paperclip, files] - name: Template OpenCode config - template: + ansible.builtin.template: src: opencode.jsonc.j2 dest: "{{ paperclip_base_path }}/.config/opencode/opencode.jsonc" mode: '0644' @@ -127,43 +86,71 @@ group: 1000 become: true notify: Restart paperclip stack + tags: [paperclip, files] -- name: Stop and remove old containers - shell: | - cd {{ paperclip_base_path }} - /usr/local/bin/docker-compose down --remove-orphans 2>/dev/null || true +# Idempotent: only changed services are recreated. remove_orphans also cleans +# up the old paperclip-nginx / paperclip-certbot containers if they still exist. +- name: Deploy paperclip stack + community.docker.docker_compose_v2: + project_src: "{{ paperclip_base_path }}" + pull: missing + remove_orphans: true + state: present + register: paperclip_deploy become: true - ignore_errors: true - -- name: Deploy docker compose stack - shell: | - cd {{ paperclip_base_path }} - /usr/local/bin/docker-compose up -d - become: true - register: compose_result + tags: [paperclip, deploy] - name: Wait for Paperclip to be ready - uri: + ansible.builtin.uri: url: "http://localhost:{{ paperclip_port }}/health" status_code: 200 - register: result - until: result.status == 200 + register: paperclip_health + until: paperclip_health.status == 200 retries: 30 delay: 5 ignore_errors: true + tags: [paperclip, deploy, verify] -- name: Wait for nginx to be ready - uri: - url: "https://{{ paperclip_domain }}/" - validate_certs: no - status_code: [200, 301, 302] - register: result - until: result.status in [200, 301, 302] - retries: 30 +- name: Warn if Paperclip never became healthy + ansible.builtin.debug: + msg: "WARNING: Paperclip did not report healthy on port {{ paperclip_port }}. Check: docker logs paperclip" + when: paperclip_health.status | default(0) != 200 + tags: [paperclip, verify] + +# Requires day1_deploy_nginx_proxy.yml to have run first. +- name: Publish Paperclip through nginx-proxy + ansible.builtin.include_role: + name: nginx-proxy + tasks_from: add_vhost + apply: + become: true + tags: [paperclip, nginx] + vars: + vhost_name: paperclip + vhost_domain: "{{ paperclip_domain }}" + vhost_upstream_port: "{{ paperclip_port }}" + vhost_health_path: /health + tags: [paperclip, nginx] + +# --resolve keeps this independent of DNS: it tests nginx, the cert and the +# upstream. DNS is a separate (manual) prerequisite. +- name: Verify HTTPS through nginx + ansible.builtin.command: >- + curl -sk -o /dev/null -w '%{http_code}' + --resolve {{ paperclip_domain }}:443:127.0.0.1 + https://{{ paperclip_domain }}/ + register: paperclip_https + until: paperclip_https.stdout in ['200', '301', '302'] + retries: 6 delay: 5 - ignore_errors: true + changed_when: false + failed_when: false + tags: [paperclip, verify] -- name: Verify Paperclip is accessible via https - debug: - msg: "Paperclip is accessible at https://{{ paperclip_domain }}" - when: result.status is defined and result.status in [200, 301, 302] \ No newline at end of file +- name: Report HTTPS result + ansible.builtin.debug: + msg: >- + {{ 'Paperclip is serving at https://' ~ paperclip_domain + if paperclip_https.stdout in ['200', '301', '302'] + else 'WARNING: https://' ~ paperclip_domain ~ ' returned ' ~ (paperclip_https.stdout | default('no response')) ~ ' (502 usually means Paperclip is not up yet)' }} + tags: [paperclip, verify] diff --git a/ansible/roles/paperclip-docker/templates/certbot-init.sh.j2 b/ansible/roles/paperclip-docker/templates/certbot-init.sh.j2 deleted file mode 100644 index ebac5c9..0000000 --- a/ansible/roles/paperclip-docker/templates/certbot-init.sh.j2 +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh -set -e - -DOMAIN="${PAPERCLIP_DOMAIN}" -CERT_PATH="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" -EMAIL="${CERTBOT_EMAIL}" -TOKEN="${CLOUDFLARE_API_TOKEN}" - -# Verify environment variables -if [ -z "$DOMAIN" ]; then - echo "ERROR: PAPERCLIP_DOMAIN not set" - exit 1 -fi - -if [ -z "$EMAIL" ]; then - echo "ERROR: CERTBOT_EMAIL not set" - exit 1 -fi - -if [ -z "$TOKEN" ]; then - echo "ERROR: CLOUDFLARE_API_TOKEN not set" - exit 1 -fi - -# Check if certificate already exists -if [ -f "$CERT_PATH" ]; then - echo "Certificate for $DOMAIN already exists at $CERT_PATH" - exit 0 -fi - -echo "Requesting certificate for $DOMAIN via Cloudflare DNS challenge..." - -# Create a temporary credentials file (more reliable than env var with certbot) -CREDS_FILE="/tmp/cloudflare.ini" -cat > "$CREDS_FILE" << EOF -dns_cloudflare_api_token = ${TOKEN} -EOF -chmod 600 "$CREDS_FILE" - -# Request certificate with Cloudflare DNS plugin -if certbot certonly \ - --dns-cloudflare \ - --dns-cloudflare-credentials "$CREDS_FILE" \ - --agree-tos \ - --no-eff-email \ - --email "$EMAIL" \ - --non-interactive \ - -d "$DOMAIN"; then - echo "Certificate obtained successfully for $DOMAIN" -else - echo "ERROR: Failed to obtain certificate for $DOMAIN" - rm -f "$CREDS_FILE" - exit 1 -fi - -# Clean up credentials file -rm -f "$CREDS_FILE" - -# Verify certificate -if [ -f "$CERT_PATH" ]; then - echo "Certificate verified at $CERT_PATH" -else - echo "ERROR: Certificate not found at $CERT_PATH after successful certbot run" - exit 1 -fi diff --git a/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 b/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 index f9f3ef0..ab05ae5 100644 --- a/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 +++ b/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 @@ -3,6 +3,8 @@ services: image: postgres:16-alpine container_name: paperclip-db network_mode: host + # network_mode: host would otherwise expose Postgres on every interface + command: ["postgres", "-c", "listen_addresses=127.0.0.1"] environment: POSTGRES_USER: paperclip POSTGRES_PASSWORD: paperclip @@ -23,12 +25,12 @@ services: env_file: - {{ paperclip_base_path }}/.env environment: - HOST: "127.0.0.1" + HOST: {{ paperclip_listen_host }} PAPERCLIP_HOME: /paperclip PAPERCLIP_PORT: {{ paperclip_port }} PAPERCLIP_PUBLIC_URL: "https://{{ paperclip_domain }}" PAPERCLIP_DEPLOYMENT_MODE: "{{ paperclip_deployment_mode }}" - PAPERCLIP_DEPLOYMENT_EXPOSURE: "private" + # PAPERCLIP_DEPLOYMENT_EXPOSURE: "private" PAPERCLIP_ALLOWED_HOSTNAMES: "{{ paperclip_allowed_hostnames }}" DATABASE_URL: "postgresql://paperclip:paperclip@localhost:5432/paperclip" BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} @@ -50,43 +52,3 @@ services: timeout: 10s retries: 3 start_period: 60s - - certbot: - image: certbot/dns-cloudflare:latest - container_name: paperclip-certbot - volumes: - - {{ paperclip_certs_dir }}:/etc/letsencrypt - - {{ paperclip_base_path }}/certbot-init.sh:/certbot-init.sh:ro - - /var/run/docker.sock:/var/run/docker.sock:ro - environment: - CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN} - CERTBOT_EMAIL: ${CERTBOT_EMAIL} - PAPERCLIP_DOMAIN: ${PAPERCLIP_DOMAIN} - entrypoint: /bin/sh - command: - - -c - - | - /certbot-init.sh && while true; do certbot renew --quiet; sleep 12h; done - restart: unless-stopped - - nginx: - image: nginx:latest - container_name: paperclip-nginx - network_mode: host - volumes: - - {{ paperclip_base_path }}/nginx.conf:/etc/nginx/nginx.conf:ro - - {{ paperclip_certs_dir }}:/etc/nginx/certs:ro - depends_on: - - certbot - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:80/"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - -volumes: - paperclip_data: - driver: local - \ No newline at end of file diff --git a/ansible/roles/paperclip-docker/templates/nginx.conf.j2 b/ansible/roles/paperclip-docker/templates/nginx.conf.j2 deleted file mode 100644 index e0999bd..0000000 --- a/ansible/roles/paperclip-docker/templates/nginx.conf.j2 +++ /dev/null @@ -1,86 +0,0 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - client_max_body_size 100M; - - # Redirect HTTP to HTTPS - server { - listen 80; - server_name {{ paperclip_domain }}; - return 301 https://$server_name$request_uri; - } - - # HTTPS server - server { - listen 443 ssl http2; - server_name {{ paperclip_domain }}; - - # SSL certificates from certbot - ssl_certificate /etc/nginx/certs/live/{{ paperclip_domain }}/fullchain.pem; - ssl_certificate_key /etc/nginx/certs/live/{{ paperclip_domain }}/privkey.pem; - - # SSL configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - # Security headers - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-XSS-Protection "1; mode=block" always; - - location / { - proxy_pass http://127.0.0.1:{{ paperclip_port }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $server_name; - proxy_set_header X-Forwarded-Port $server_port; - - # WebSocket support - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # Buffering - proxy_buffering off; - proxy_request_buffering off; - } - - # Health check endpoint (no auth) - location /health { - proxy_pass http://127.0.0.1:{{ paperclip_port }}/health; - access_log off; - } - } -} diff --git a/ansible/roles/paperclip-docker/templates/opencode-auth.json.j2 b/ansible/roles/paperclip-docker/templates/opencode-auth.json.j2 index 40fa935..fd40afd 100644 --- a/ansible/roles/paperclip-docker/templates/opencode-auth.json.j2 +++ b/ansible/roles/paperclip-docker/templates/opencode-auth.json.j2 @@ -2,7 +2,7 @@ "vllm": { "type": "api", "key": "{{ vault_vllm_api_key }}" - } + }, "fantasmic": { "type": "api", "key": "{{ vault_fantasmic_api_key }}" diff --git a/ansible/roles/paperclip-docker/templates/opencode.jsonc.j2 b/ansible/roles/paperclip-docker/templates/opencode.jsonc.j2 index acdac09..b10554f 100644 --- a/ansible/roles/paperclip-docker/templates/opencode.jsonc.j2 +++ b/ansible/roles/paperclip-docker/templates/opencode.jsonc.j2 @@ -1,9 +1,9 @@ { "$schema": "https://opencode.ai/config.json", "provider": { - "vllm": { + "illuminations": { "npm": "@ai-sdk/openai-compatible", - "name": "vLLM (Local)", + "name": "illuminations", "models": { "gemma-4-26b": { "id": "Gemma-4-26B-A4B-it-AWQ", @@ -11,9 +11,9 @@ } }, "options": { - "baseURL": "{{ vllm_endpoint }}/v1", + "baseURL": "{{ illuminations_endpoint }}/v1", "headers": { - "Authorization": "Bearer {{ vault_vllm_api_key }}" + "Authorization": "Bearer {env:ILLUMINATIONS_API_KEY}" } } }