diff --git a/ansible/group_vars/all/main.yml b/ansible/group_vars/all/main.yml index 979d54c..3f79c44 100644 --- a/ansible/group_vars/all/main.yml +++ b/ansible/group_vars/all/main.yml @@ -57,11 +57,6 @@ subdomains: # unauthenticated, like any block explorer - the data is the public chain. monero_explorer: exmrplore - # Navidrome music server (music-box, proxied via vipy). No basic auth: - # Navidrome has its own login, and the Subsonic API the mobile clients speak - # has nowhere to carry a second credential. Same call made for monero above. - music: music - # Read by plays across several groups, so global rather than group_vars/. headscale_namespace: counter-net diff --git a/ansible/group_vars/all/vault.yml b/ansible/group_vars/all/vault.yml index c5dc402..9c3fb3e 100644 --- a/ansible/group_vars/all/vault.yml +++ b/ansible/group_vars/all/vault.yml @@ -1,214 +1,191 @@ $ANSIBLE_VAULT;1.1;AES256 -66356262646437623239383039643762363139653565356263623438303833663431396566353163 -3631323563343638346464386661346235656130383238370a353236623366313561646334363639 -35346232376138666430363437346638326264646562386436323962633630663136626462343762 -3862333139646636650a656337306533313436333239653561333231363030633339303835396637 -61633763313464353933306333383061346563623637383166646332303237653065346233623134 -39386130613866386235376266643062653833643132633235393931636366376164306331303762 -33393562303738393364323465653163633730633636343764646337383733643362396132303063 -32336461396561663063633137613234356535383863346334666465643437613962376439356433 -66353532333933343462636163353637613637356530333331636363616265376263636538656566 -62643137623933363237343865613131383766633561636664616434393538646332346132336630 -62333361626339626638333666393735373834396431343965386532313834353837336664306530 -36303138373734343735653637663466353435306166316661353738353035366535333265626330 -36396538353566316634376662383564323861303063386163663061323435656261316439336539 -65323063343961646435373134303039373139333265633838313636373463316231633832306261 -36623934343731343938306332356261646264613063393834336332666230626534653464363335 -66356331666434623735613333303466656263313964363030316231303330303735373135653763 -30323635303435656639653037366430316330333033306339366361353765656337316561366263 -38613265363565373832636239396362616435303536646465326132626239353361613137626632 -30353764323430656631366134373263343861376666346166363131333137376466393438346461 -31613435363637306432343437653636356665373838626361306332316664343266356666353238 -35303464326633326438353730393462373437313864326638323262323034346464643038386462 -34613435326432663566386331333430663336656239353131633261626637613035636662346136 -33626131626435316566393831333534393663326465633531373932626631613063666165613766 -63636463656435386333323037656533306331646561363061613537663738326162656332393161 -62313338376630643031666630663337646439623862306663303335303232306161353336643334 -30386537336265383334336431653637303836376261303038653739396263666161653365356133 -62636638393465363239616264316331316139363063313431323165313237646437643361653539 -63393932623934313062376137626266363330623766646231353561646332353565643638356633 -39656565363962653038393430363666643361343131323231663566343035643631646433306464 -64343735393937333036623139303531343338373162396635613863316466393235343865333065 -63353662313737353737353736316261346466366666666536653064356130326334326137383933 -64306331663430313838376266613763343537613965303937393364396238663266363539313531 -33616133646365396336613234636630396239316633333832653138393964616261643531663836 -36666265386134383430626637643766636439393636336436366265396239303335306563653962 -62323763626265373037343766386433646637633838643933633738333037633336646663316335 -36373534663439306137353236343565636538636333313239373936653263366637613335663233 -32306536643866656461343666396565373661353561393665643162613539633634666138626266 -35613663376465626363373864626266393335396461316338653262316131643633666361333537 -64366533633535313633306535393732383436333762616539646463366665623866323463336263 -38313365326338386664363538633232343636303135356230393065613238386366323435633931 -37353537313330396463383233616532653139376263346264623366383339306139386231636466 -35383761373733386133386436316431613265306264656630353561333736613734653164333031 -32643237366534643062653065333632343563633034663266393961353763663534353466383561 -38373332323135353932343231353433366637333437636238623235663836393735346230623438 -37616639643231396564373033636162616234366262333461343139363539316332333431636565 -64396639383462386239613035303565376163633637653137373631383864353532373465626630 -63636531653063306131623735393963623836313566643565386336636565633130383264316533 -33343030316365373164663661306161363062646361646332393935373533626331623163323038 -61316666366263313531373530366639333364336563623162323938356662626565383363303533 -61323730313161633035646435346234366661386339346663363438663862636361343033306134 -66396365653634373235623837666537323230616334663263633135316433313734663863333561 -34326134613335346661316632353764343235326630316535306666326662613436383764646366 -33616135333038333834656439383733656436663433303536313961333662366564376338326661 -35393839643663333130383965663538393931363662336165636435643132353461343266363265 -66346635313337346661646461386336386461346133323435643939616234666665616261313731 -38383265643830646364373164656133396132666236313265346337643939613831303366393530 -63333330346131656134366237323661316534323330306330616564393130353136306461666361 -62376637646230623264363437313161396130616231346133346665303834623833663062663436 -30643736376638303065356161366561353565343139343731313263393933313239363363636638 -32663932626332626366333635616335316235363561373539623265663733373030373439373831 -65333763653635613964396437333833626535643666643433393031336461366562333934666234 -62383632303436633965643430363763353361613762306361613066356136383131383232313961 -30626531353933336563656139316338616565303261363933306164393966343037353431653233 -32636631373966323838333666313964323933306638653162646166623237343662646134383337 -37383039383531656337626334386638613133343133633637343738343731326663356666333161 -61323765636135306563323064346465633537613361383761393361353363396237333438306166 -35386338356135633365396661356535623232613765653931303063353538643130323439366438 -39363936316263333562646133646464346362343761336662353764613261396364626164313664 -31383839303361323461663662393736616363646232666633323134653138306161323735386638 -30333365663064366535656134323338663566616163333634353939363533356364393563363333 -65623838323034336530326363383237373563326334343066323738663833353930663930373965 -30363766633137396561376636636566363862353062663038653534623430643766633631356562 -63623762343433626363353239376539633136333038623338303961373938396632313966666532 -33633865626462636137343436386661353561623035656435623566613530623163316465383664 -37346261616239653138623262623930316432633531313037353938396164646534316564613238 -36643137303965316238346134323639386261633338363130323134336632636564636638653861 -38346566643966386465366535393166626463313039613236393831393732613537336530393936 -31613234333836333732323165653931336565316564346239336663363736323763623962383261 -65626332626565636162336264303964323139303831656262373161303861393366353237393136 -62303361636131646335633733373966626135396630623133623733373565306234646165656138 -35383065613365363438396332306236646432663731326666613838643434393866623163316631 -33633931373135336438643735646266383462386431666330363066386538386335383231653334 -64323366363032386662333662613562333864356238626161366566356564343339343838373437 -30363133363136663036656461343534666636343932313336336637376665393564313234373430 -39356362616539366237646363646364326664653232313235653831663539353530393261346532 -38653134623732313466386635313730303639613561323665356633653836393237333737323864 -36323736326239636565346536646566656138666630396366353930383237333338626532663131 -64366339303230663530626632623066613030663533366539313866646466373439663831363936 -36366636653032343064383661383664636463343165326532653861663537633238323937363839 -65333030653566306465643035633162393534323731346330333861353236393732346130313034 -64303736323030643533633539623638643232306435663238393566376564313162633334613462 -33363931383664633433363032643536356135626662653733303339656339373735643732626535 -35316131356135393933393462336163373062653133356133393830393638313838396361336635 -62323463333464353939346233333462646161633036643332313337353065376561336630383163 -38306666666634393766366436653436623138363830343636323833353365333762343533633835 -66363730383932303661313831663566613830393530616164323963636339393339623065316534 -38373662636330336632653335316463623935653961363566396132616433633831636334306236 -34393761663962623563333534383739613965353033646235653532303330343961656639333337 -31643462663538313930383038343964326463363935396365343466643236653738626639616434 -62343337356239316162383564666131663535313633373062353030336439396533376235386131 -31653865643931323364356566316435333336316166643063613164643266313834323235353863 -34623936613366303161353165386662376465656366656561613466383830326162336238656236 -30646632396231653263373631346330376461303231373733646231373462623131303633616166 -37366435353830303234393962663334656335356661333437363637663664323430623135346262 -33303233343362336261633663336635366233313466636233333836663464633637383964313133 -31363461336237373337353438396132616134303432336533653737333538383534363735316334 -62303666363532383131383833383536333733306163366339653038313930646531653565386335 -38313561303062336232333634343866393763636131333665396430306432373734613834316636 -61306138626530616230343462613036323166656334656637616139663030623932386132393762 -36363364343737613165376365323361363233616532643866303632653063343431613338373633 -63303263626434326539316439323239616632653632346538336535373336623739323931623462 -35656365336630323035616161656237353135373366376466326264323863336166633438663834 -63646562386339363235396163646334333238323932383230643964366466323064313138323464 -62636239663635336332633334656631383261306437333963363464303430666462633333666638 -37633830663439396133323861303735346561376434386131363864316332623137376139376231 -30303765646132306261363335366565306564373261663336613336336632616231363366316135 -65303966616439643863326662656439623938373833323738646535636230663136653266653533 -63326235376633333366373436333534336436366434363461393763633437616262326133623563 -37613161303161356632316335616639373564306161623339353066383163303431313333306635 -36326435376661343237303633316462613532313232643461313065376134393338613130336161 -38326463303562666662613361656161306139653431386630303561626666613037393238313463 -62623533396532326332326636346134616236653638363837643131343166333564323538653638 -63383261383265353331663864643933373562396565346163396434383765343962336363353261 -38353034643436626632623235396261326235323937646135326532313561396361363062333366 -32343666343163636233646466613633383034653438646262666133643830333834383233343136 -33646437396137613466643031323538626332613066333065643237353263636435393964356464 -34383265303364663233326432313230336436373866303435336666343832383737303030306534 -36383036363937393965386431343561333961663565333565333261323033616630343130613934 -32383166333063623235333464343864326531333165346261643131393264346534393035393431 -64636139383232383332623866323363623666326361386134353030376366323039393033653061 -63383338303734323637386366646365663032636463336261643533663638343661613462373736 -32663566666133656165396233643334343335333731333938376236633330366262366139326133 -32653163343963333739303061643936363861313938646164316564393261663765393462613837 -31646636326330663464363034376230393739343961653130303166366233396262656634313061 -65646664373161356134616165383265346633376136376132643234623334353563323539333065 -61623265343730326164373063383937323766363466343466316364373935626138343938323261 -38323739663131346138623663393832346236356631633133313436363364663536316561623631 -32393562646666343537663039363165646337653564363065353433643031353865386361613534 -37633639303636313261613563353136346465326163623361323230306364396362373966336262 -31353565623239343564333533656236656666316638646666376138373164663138633835366162 -37633036346562663261343731366161613361656534343839303062373232636339643961666432 -61396665656638333435386338383638323664313139656331336131326563373235646539633835 -30393235316564656131303932386336366661373737393264613364663030363862656462646463 -33303766303166336132626265653431313263363131653835336634356632666230666235383262 -31653837356536666266383562633437376161653664353533656331303563626539626161653562 -64333337626430616330346233613566353330383361353234376236656336653735663838343333 -37643162376630373531396138643861326562323133303363363837306138373434306364356364 -64623037643737663138626262326630323638376336663062393930663566316632323862653135 -66363664336431373966663133353030393863313538366632643932363438613035326563323163 -63343565373862316230643166336438393464323032666334653631393761303337303861666134 -36663637346136623564613233343432383930616339626161623933323565623861653939326165 -37383436353533613736323237666433316130613464396333343965393031333138396461376632 -61373166643766376234646436653566313637326434363261333831336136306137333061323035 -66303335363035666366386364303861383231366339643666313636623733616562333539643465 -32353566326262326637393434656133313963646435333135613630313865373430366265666632 -37316539363439663131323835313761313765653564663337346462653332613430373037333064 -64323731623566326664613634626265383661346462323463303638346464643366663333336339 -65626265663230346338633764633436653339373031623438633638326137613635633239343834 -35373132376133363861373062386235353836633030323236343666323134326135336231613566 -34326263643163633935643065306165643564663831633561353061653331326566633664376432 -33353835366265346266663836326465356166626432356663366566313962663862363930336334 -65323337386231376361343933353865633330613466323337306137633762653931313331313339 -36663965353832386662666433383934303737656336383432633632646135383835336631373861 -63646164373137316662373330333465343134633466396566333334663036663039373934333832 -35343661303565646464373834646234623837363562386463383237373533316638666663613633 -30313437626231343062636437363836386635333862373362643532646335353863323936663034 -30316461373330623130353563643035303561303666393464636533626562633165393236663936 -31313061303465633332646363643866306439383636356366623565663437313365376331656666 -34333930333235306636646664326561613165633233353937666238646536323131313232626366 -36623237613635343134313366346661656433643136353433393532303165326564646331333035 -35353365633039353161663337306463303535643062366232336239656162653532306539333035 -65373735373832326337333962643330363338656134303637313337393466366263363264343631 -30656433643562393537326361323733643639396537303065623936336230346664616534323262 -36313864396364353836373538333130303731383437376137383938383633373063323936613464 -37613166323432363232313834326565663265336138396335313230303935396266646438323833 -65306666636338373239346332613464373235613137643133626261313031326462356138356632 -66363465346165306533613636383938636531656631306139666237396632646634333937623032 -33613439656637383537356364336265653931613835333734383836343930313838626564333065 -35363735313162616238643438316535356663396339316331633232393232316131396432653639 -36303335643534376536383331323562353139613130383330636334636261326462656565643138 -63326365363163383932653562363833643761653839306131343737353633313737353732633463 -62326563336339383661376665353230613366646132653364396338663337326136333263363838 -35626537666238383435613431666365303133373166386136353061373765313632343665306439 -34356139363636623639626633623030363933303539626665383733363638303832333561653130 -64393437386362333731643131663263626434666661326533386362306362653732376366626139 -65626662323232636134653334366639386164653535323435613863353332366630303636663339 -34333633373737333732363664353437613762383064316134393139393539306564396237343332 -64636339643030353365356136366264353935643830633237316532316265316635393664326533 -64343030396334326135336465336637623832363065353163316465303537643861306632336535 -31366338323263313965643565656430366566393130326333383862313239356437623232613865 -63383863316639346632323834633739356464313865376463323633393365653033376431373138 -61363035653233373036666531303331383337353839363563386333313730363664666463333735 -61383638363164336265383664376463616536383533666265383961353961303833353265366633 -32646330333336613631633830633837346332653437356536363532396438326364623465383133 -36656231343734643830383335623930343132386336666366623161666637623862363033373236 -32666332316266623230666231663162643262653231313965323137383532633663323239396139 -37656533373065623161623766333335353965373731346639613566343236363239313537636239 -34626533346138613764346161383337623265353563343066303236333534316137356636396565 -32616136306665346566616565346162613736333836623363346232373932366365663531653435 -37623539366233366539396361386163643161653731353232313034613135373039653339346637 -31623138326133366333656661393837653939343064656638616630346462363065623139323131 -65363036383433663566626336353432613535363239373264356237323032323634383662613763 -39646161363639326330346332353965366330346632303939623136303238323431346361656462 -61623838626139636163353430636461666239343337373132643334616234616339313761353331 -36643165373834373437363165303861306135383236303030626463373235393234313765343563 -37353932366430666539623230373161303935633565323366363232336264643961383835333066 -32623366663965316662383036306462616465353139336333643936363838313538363832363330 -33313861313636373831656431383434363664663734626363646365666135376331366335653064 -30386235313037363365363232316338613634643235383037393635333166346632356435373039 -66663862373939346434333130383065386135306661353066343330666563386136376562316134 -63346435643132316434363666343964303335663131383831373430653632663638396333393932 -36383664376666393761643730306336333538376637633135616166346432636464336434323031 -3966 +61616634393062353739616362333531616534666431633234333539376337313235366337346364 +3135643764366262633636353735613233343361363165390a303638313465643332363839326530 +66623032363563306135643838626435646639306438393638353133393731386139336661383461 +3965353830373536360a653364663734333762616630313563656335333933333566333334393837 +64636438386264316333633839623736333932356365643639376262353564363639303438373534 +37346139393038643332626337646430376165316133343362636235623838343637343062623431 +35333530376430643562656233356165313065633865343439363238663966366332346536363430 +37313261363434376438306333373161303465653132353864366562363235376630373262356633 +64393830643037346565303435633966303234666638653833353138346464383462383136313433 +38356663303231313134623862643762633762303364616433623963326463326232333662643839 +35346435643234663838643934376461393631666363333435623965376239353137613533383033 +34663662666362613661613833383166343965333939336631363633343332653064653761353063 +31356263396434386433393332353532373663396431313539343138623564393434373135616239 +39353163363063323064633935346134663165626265633136633833643538616662333663366664 +61323866313266626637356532336166643261343662636464346566613531626532663961613238 +65353434653466316438343631376332316566376438623835393438313239326339323564623564 +37313938396562623865646338336634333637666538656139646539376531656464653136663462 +30343537303230306331386139376261396162326437623265373336356163636562623932653562 +64353763646636363839613365386233316262356339653332323565383033613561356236666336 +39616636353530636434366430643239383164376139343064316430396431363363383166346130 +61373735313633313639323933646632393231633935326439316466623438393233623139626139 +38353333376164613765656164363436613831663333656239346661623031393238353765356233 +65336632633361356432326339613766633938336537306261633236333835646335356365393330 +38656236363864613862643365383265633134643338383033643434376530303462336564383066 +66663061646432333034316431353236323938623639303263393537313939373162633838343062 +32316230613033363931343162386538376435386139396634333061313930373335306663393938 +30643466303338316663613563636239343432363964653438666662356437623962653266346434 +64616136643835396430306633313736623736316634313466366538663437633437613963386231 +39666565366464326132643836366431653535343162336563356664346166313532393664353139 +39383037323234323038316632383766383266393663646438653038643566376133636237653733 +38623438343432323664303461373930363437356136333235393262336336633266396531663638 +30393335396537356635343730376532363261353039396639653966633033383037656536646161 +39653531303962656234343634346236373362616634313664613764373439643734306639386461 +38663364613365643030616430356139393362386438613838326236643063633538346262626133 +33666537353862353461393939656231666330633434316261656134363062353835663833356435 +62663037383266353037326431376438313165343231656366306265666464303132613133303662 +39646462333439633065356137633932336162333438356337663637353033326362313036303564 +38643662633531383539393064356535363631336266316334386465313965376665306163393664 +64643434326561613333353134633166613464393537323263656466613239343562653636353764 +36343365656636393031626231343033653764393361376532646139613166306561323364356364 +62623838316430616636323966616634343732353031393665356137393532383762363565663038 +35323930633933626464633966343865346466353465626466663435316137366435373734363661 +33663939366165313939643032396562326565343961376366313730333735336333353164623266 +30306261363230306562343433656133303662306563646166386531386439633534623632373932 +30623361346632346463333661366539613233383337353961383564303438643965636235396163 +63363264383762303631613033663039626539336566653062333632383534306230396139613234 +62633862643535383536353537393431376431303262323332613731636534643033303635613030 +61653234313732643563383639303633343266623761623435373734613235656263353864353434 +63353039386665623164626666626330623730616438373832303337633364633735613364623131 +35613433366361616239363464343937363566383934313863636231373434636331383939623566 +34623933373434346561633331653163323664393130336665393465303162396630376236653262 +39393465346632323435636333613361353539643238313239373231376436313631663065386262 +34663637386632396137353261613666316564333636326231636137613235623765313137306537 +38306161633731343838356635613038616363653833623634633061376364376534323532666337 +65366634623331323562323562613735356235393434323063303766653334363236613036353638 +35353438623930366336383062303462646338633939393639643832623730633562306338313939 +35666334336334653534646662316534376235656364356563316632363130356635333336666130 +34616639393834373362323930613863383833663161316339323465616639633531376232303332 +33333333643934623538633738643631386232643932303138383861313261306261323532393233 +61353431373538313762653839343861313337643633643032333765396466393831393162306436 +32393832363638333832666364646266376634663031333932333735643831623361323564386261 +61323937393762613531646564363963626461343262373839383938663661626562613837323133 +39646632316666396637646664356230356131313062363039656163613238386334636636636636 +38363463633733663132366239343766353038383962353639313562343636623261376566653635 +35326338663935386132333035373362326235643864336162303063623539656439306466663834 +38333362353630363835613464303131306333313865623236633362623832633062333565343762 +39336161376661646238313839633165666435346433336337633734353166636566326464666330 +37386536613430343139653134343963626466333734356461323631623761663639633532376133 +61363632303566336161653430613735316562656334323265303163383939323564356465636530 +61626262653461386332646566306666323434613162613033373730303265306231316630346239 +39343735363632623561373838316334393166303365363766663965306161643365656132653531 +32613662663362653366633237386362623962323230303538633732663061376334393666326535 +35653038306233626133363732393964623630383637363937363335663836346564636639383266 +32326433316236376363333163346234303938626635363464636563653032656361323138326530 +36636163356632613738626136376563656233313034666161323362313533366461326132316162 +61646335383662313234666332623932396632333435316466346538313630633038663231373764 +39343933613361336439636536356136383535336262613538323837633563633038306231663632 +33396366343466383538306638656234383930306539633865373031343030333765663735386438 +35363232616533656632396661386566643738353862646535323533323237636137646239353664 +32353261366334353933376436336533306531356462343535346664656435626466356662613032 +32656666336235326165653861326431653335653131663034386166373434303936646634366635 +38356536343664363836616235326465616131636132643331373366656164653638613066333737 +38373864333734306234383130376239373238636262376664613865363265363337393031616566 +35336436373535613535653330306164373462313839316636326534366163326538653633376337 +34643039373461393565653838323061656330346632316133373064613661306365336462646237 +62326134623063346431643335313166306461653239303537663132643239386636653363323662 +39393262613766343563636561356362653665313234366532626638346264316363343264313435 +36386531656534666462383337343835313638376438653165663839393433376332623938363738 +35356538383136333534386531376530383936633463363166326133303264373534613066356538 +34373637303333333836386238303061303834383331653735336233666164656530656666356533 +39343030643236613831663465323735343737623362383734326162356333323864333035366433 +63316264613337363136613062663263313731636335383531383933623339666134643539353330 +61333330336132636666383561356565346563393937653539373431653561653664616137623133 +39336333313330336137346238306130653730326130323163306464376434353736623232656134 +33356435663762666266393461363531396334313733333132666363316133346531626564626334 +66356436393062373364316439646235666336333039383137353764303763636461663638333362 +66306430396530636538353965333862323363323832373033353530306331653935396134323634 +61353964303666643163646464613737393863373236613532353761636230336633373461343266 +34383031353734346465613030653639656366313530333265383632343465393566386161373633 +34313563636265336239323362316432383637303134376331633039333537613461353135303934 +66366334663032336330386632656162643933316666643562616564613633303231666536653536 +61376534663632316338653231653561643338356661353232633431623834396635383034366438 +30376161663339633530646539313132383536303931333366316631356135303933316162373730 +38343139653932666335353437613734366530616261343134386261313330303766666136616532 +38663635393236376661623862646265626239656665323332633532616536643237333137343763 +37663432366337633662303931353939346462633837346638353530636132313436626161633865 +30653065666261363238366534656265663039373962333938633335373163383261363434646437 +35356538623765343866653065306164363363616137323065333062386435393539303930363239 +62653638303766343738346437323536306139303261393863373035336662316566646565316662 +30633464663666383135663365636436363132363437633933643464626339643865383066623039 +66356338633939386337353135613066343236356262363164666466333462383364663134616433 +38626332623530633765656464383136383466623561633666366566316461356437626165316166 +65626339383133623439366662373666346539613231306564643731383935633939343865613534 +66363934373433363134323165383830643535613139313139316137643532303562393432383330 +61376363363939633835383161396464613662383961366634626463346430303631653930613430 +66616132616364623261363731343437633032383361356433333531363964633962336338373338 +61353134616332663135663737613230666534656232623933613862326366393538373737393838 +30343037343432306437663035613831316432336431383938363338316437616164376435376261 +66363664333031653962383630656430646238353337616535343535373935663561306536353165 +65343235396537366635633638306135383164353562336331376336323633323338353361353061 +65623431633731303936303732613964346139306336356461626466343132383162666337333664 +38636631636338353339316666643436386537336231353332663636383337343831323939396636 +30663634653733623530343537636137623332666666343433643830333737336639313064386333 +34656362613863323262326130666361373434336234366337663236663739383366373635343862 +62656365366533633263323963346363396237393462653633313133353935343631393032303566 +66326266353264306266613936333734643631376637663137373232653962646534643565326366 +64646634346534613730646430333432373464653365663666346232373332346231306563636136 +38646565366661613161383530326261613537366633323239656461343337363139353164653935 +66356636653232643035363564306438396233376465646462316463333832636361363137363364 +66653164363836333261386437373864666438383338383963616662386631306530326535303666 +37393037313539393535633333663662363164323635333635646638663264383533303032303561 +34613563353237633761363435636138616462656263353966636363326533363235663761643033 +63613934323330363732353465316261653964656430383532623833653663306632633832653763 +32333932663362663664663535363566656665653839386363373066343336353264663136326136 +66383461626137353430316266356366393937613234656261646632366130613739353266613163 +30383731373137653137353663643866343632383937613136666361626435393331313130386230 +33306439633138306331616165386239643765323765366530336337623264343030333234336234 +64333333613838333734346361636162346564656334396139373535333830313037383864623561 +38336131343532663761326635633531323766363963663763653630643232663631613465333132 +35306635636437323835326633616665313133393162623737386538373364343865383634636463 +33626165623962656432396237653665346464663066633633633564313266343662336333656166 +34646636663864663439356364383031313065663433386530663365353937626366663334346434 +35656365316134306161656133653736306537613133386634386638376534376133346237353765 +64363964303535653037323566653838633231363132666237353765643137383030623930393931 +33313935663366313362656137613865386532303737306437353437336331643435323030663666 +66383139313333633538373836396263353337316264656631376236346466353765653830366366 +66386131336431336466636166366139396337386135636262316237306561353563623061313266 +33343563393031303133393163653638643065636636653766613836386438626332373965383864 +36343936613738336132396265663130643739373264356530636363633239346637653136343761 +64646137383235636231313736343162353237636566663830373739636536393864646639343564 +61323039376537646531633030626665323537366233353939653665326666653263623431356166 +34343538613031336561303165323134303862666139633937323331643930383938666138346230 +33633537666431643234336134333333633664333564396139363136363434376130323632363833 +32363764656133653462633132366630396233613463643833353038316232353962623761373966 +35613132306662663937366464373034336531366262643939333137613031666162383434313664 +65363332383330623863326465353039306338393230303165656366386161656239316161336633 +61616434363637376131316135643336653636313565613035643038386332356263383061643262 +63333439303736623835653636353261363532646266663331313566363562343730303436353636 +63636237343965316366613137353062393832373133316438656433313132643061633130396662 +65346530313962306238373634366266386232613566656337396663626535393964323737323939 +66333165356138353362336166376466303965663439636432653862316433376264376563313137 +30653565386636363730623630646633633832306131626235613862393239386437386561356663 +64303862616530393838383364643266633336326665323961643935356339316564316332633635 +32323239333339633838666233376165303061373465376133383863333232663065626462303362 +37613638303266356537633965323966363331383130613963363933656531663031643030396632 +38343838383731346536616364303531373030643730626165633464646137376439643637633636 +62346664633931376438313639613165306363356532346566336336363662353233613134346132 +30646633363364363234396435323037306663633863383530383034383338636639326362626639 +64313532313939316562353561323534313438346530303536333066393863613136646664353264 +65663530366238343732396430633138613664653962366338643738623636373031363839393534 +38393439393366396262643461303361613666336631313037306231363537346364346432616539 +64616431633730333233616362363638613837356638343131376262363736666532356532653330 +64353431663261623033323337373932396436656633396439323162626563653630633239306233 +37633939363536396439643663623434316161383938393864653437316535313433336461383065 +32396536313835393030313136356530623539646361633935333362373339393662393335353361 +37353366666264613563663836666332353561666464623561653238666537316233343838663363 +38616463643935376233363031323838393163306465333030393331663231623061646530393237 +33356466363261653235336134376166343537643065663235333161383366336132316666383731 +39326662373162313861363132653531323165393939313461663262316337333762393134663030 +35313762356234396336356336393937316262646264306361363235663334333663363136383430 +35366538386335633038326563623833313031383762653266643034373838653863363835383466 +37323138316564383262356633646262623439326464633530313639313637326136656139376362 +35323866663262373938343863653665373139663565623730666366643630636566393266303261 +35666261383464393236353964623234306637643838623535313264356364366661653639303662 +37616531626435323664636538396439373064333563303637303562653763323935666138383338 +32326630303465366332656165343036336630383163643361306136333361373738306238393932 +63363331373434353563353631306361383431636239376464643239326531306130633463653665 +64636536383430613538623663363066646437666537306161613265323936663366353864323230 +32666333326630636433656166343331396166383438313866306566356238653061343461653035 +6236383630643231646264333133343738663663323332336536 diff --git a/ansible/host_vars/monerod_box_local/main.yml b/ansible/host_vars/monerod_box_local/main.yml index 5a1b130..181beb1 100644 --- a/ansible/host_vars/monerod_box_local/main.yml +++ b/ansible/host_vars/monerod_box_local/main.yml @@ -29,24 +29,3 @@ monero_data_device: /dev/sdb monitored_services: - monerod - xmrblocks - -# ...but on THIS host, once a day rather than every five minutes. -# -# Both units are a Monero node in the middle of a multi-week initial sync on a -# rotating disk. There is nothing a 5-minute poll reports that a daily one does -# not, and every failing poll was another Signal message - 83 of them in six -# hours while the explorer was wedged. -# -# This is safe to scope to the host because monerod-box runs ONLY Monero units. -# Do not copy it to a box with a mixed workload: the timer is one per host and -# iterates every unit in monitored_services, so slowing it slows all of them. -# -# 26h, not 24h: the window must EXCEED the push interval, or a check reporting -# exactly on schedule races its own deadline and alerts on jitter alone. The -# 900s randomized delay puts the real worst case at ~24h15m. -# -# Host liveness and disk are deliberately NOT slowed - they answer "is the box -# alive", which is still wanted within minutes. See infra/400. -service_health_on_calendar: "*-*-* 05:15:00" -service_health_randomized_delay: "900" -service_health_heartbeat: "26h" diff --git a/ansible/host_vars/music_box_local/main.yml b/ansible/host_vars/music_box_local/main.yml deleted file mode 100644 index 9af0beb..0000000 --- a/ansible/host_vars/music_box_local/main.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -# Navidrome's HTTP port. Read TWICE: by roles/navidrome on this box, and by the -# Caddy play on the edge host, which reads it as -# hostvars['music_box_local'].navidrome_port. A role default cannot reach a -# second play, which is why this lives here and not in defaults/main.yml. -navidrome_port: 4533 - -# Systemd units monitored every 5 minutes by infra/401_service_monitoring.yml. -# beets is not here: it is an interactive CLI, not a service. -monitored_services: - - navidrome diff --git a/ansible/infra/401_service_monitoring.yml b/ansible/infra/401_service_monitoring.yml index 94828eb..ab564a1 100644 --- a/ansible/infra/401_service_monitoring.yml +++ b/ansible/infra/401_service_monitoring.yml @@ -64,7 +64,7 @@ 'name': (item.0.host | lower | regex_replace('[/_.,# +&]', '-')) ~ '/' ~ item.1, 'group': 'services', 'token': gatus_push_tokens[item.0.host], - 'heartbeat': hostvars[item.0.host].service_health_heartbeat | default('11m')}] }}" + 'heartbeat': '11m'}] }}" loop: "{{ host_units | subelements('units') }}" - name: Register the service endpoints @@ -98,16 +98,7 @@ healthcheck_check: systemd-units healthcheck_units: "{{ monitored_services }}" healthcheck_units_key_prefix: "services_{{ host_key }}" - # Cadence and heartbeat window are ONE decision, and both are a property - # of the host - monerod-box runs a multi-week sync nobody needs polled - # every five minutes, so it declares a daily schedule in its host_vars - # and a matching window above. Everything else keeps 5min/11m. - # - # An empty healthcheck_on_calendar means "use healthcheck_interval" - - # see the timer template in roles/healthcheck. healthcheck_interval: "5min" - healthcheck_on_calendar: "{{ service_health_on_calendar | default('') }}" - healthcheck_randomized_delay: "{{ service_health_randomized_delay | default('30') }}" healthcheck_boot_delay: "2min" # The per-unit results go to keys under this collection; the role's own # single-result push is unused here, so only the base is set. diff --git a/ansible/infra/402_public_monitoring.yml b/ansible/infra/402_public_monitoring.yml index 61968be..c24df46 100644 --- a/ansible/infra/402_public_monitoring.yml +++ b/ansible/infra/402_public_monitoring.yml @@ -31,12 +31,8 @@ - {sub: "{{ subdomains.memos }}", host: vipy} - {sub: "{{ subdomains.mempool }}", host: vipy} - {sub: "{{ subdomains.datum_gateway }}", host: vipy} - # Daily rather than the 6h default - see the Monero note under - # public_sites. A record that has not moved in a year does not need - # four looks a day while the node behind it is mid-sync. - - {sub: "{{ subdomains.monero }}", host: vipy, interval: '24h'} - - {sub: "{{ subdomains.monero_explorer }}", host: vipy, interval: '24h'} - - {sub: "{{ subdomains.music }}", host: vipy} + - {sub: "{{ subdomains.monero }}", host: vipy} + - {sub: "{{ subdomains.monero_explorer }}", host: vipy} # A public resolver on purpose: this must test what the internet sees, not # what a local cache or the tailnet's MagicDNS happens to answer. @@ -59,22 +55,8 @@ # 401 is CORRECT: monerod answers an unauthenticated request with an HTTP # Digest challenge. Asserting 200 here would go green precisely when # rpc-login broke and the node started serving the world for free. - # - # Both Monero sites are polled ONCE A DAY, not every 5 minutes. The node - # is a multi-week initial sync on a rotating disk and the explorer reads - # the same LMDB, so a hard poll measures the disk more than the service - - # and a wedged explorer turned that cadence into 83 Signal messages in - # six hours. `threshold` drops with it: 3 failures at 5m is 15 minutes of - # tolerance, but at 24h it would be THREE DAYS before anyone heard. A - # daily check has no room for a blip, so it alerts on the first one. - - {name: monero, sub: "{{ subdomains.monero }}", path: "/get_info", status: 401, interval: '24h', threshold: 1} - - {name: exmrplore, sub: "{{ subdomains.monero_explorer }}", path: "/", status: 200, interval: '24h', threshold: 1} - # /app/, not "/": checked live, "/" answers 302 (Navidrome redirects to - # the SPA) and only /app/ returns 200 with the real UI. 200 rather than - # 401 because Navidrome has no basic auth in front of it - the Subsonic - # API the mobile clients speak cannot carry a proxy credential, so its - # own login is the gate. See roles/navidrome/README.md. - - {name: music, sub: "{{ subdomains.music }}", path: "/app/", status: 200} + - {name: monero, sub: "{{ subdomains.monero }}", path: "/get_info", status: 401} + - {name: exmrplore, sub: "{{ subdomains.monero_explorer }}", path: "/", status: 200} # Ports published from the edge host by socket_proxy. public_tcp: @@ -117,7 +99,7 @@ 'name': item.sub ~ '.' ~ root_domain, 'group': 'dns', 'url': dns_resolver, - 'interval': item.interval | default('6h'), + 'interval': '6h', 'dns': {'query-type': 'A', 'query-name': item.sub ~ '.' ~ root_domain}, 'conditions': ['[DNS_RCODE] == NOERROR', '[BODY] == ' ~ hostvars[item.host].ansible_host], @@ -136,10 +118,10 @@ 'name': item.name, 'group': 'public', 'url': 'https://' ~ item.sub ~ '.' ~ root_domain ~ item.path, - 'interval': item.interval | default('5m'), + 'interval': '5m', 'conditions': ['[STATUS] == ' ~ item.status, '[CERTIFICATE_EXPIRATION] > 168h'], - 'alerts': [{'type': 'signal', 'failure-threshold': item.threshold | default(3), + 'alerts': [{'type': 'signal', 'failure-threshold': 3, 'success-threshold': 2, 'send-on-resolved': true, 'minimum-reminder-interval': '6h'}]}] }}" loop: "{{ public_sites }}" diff --git a/ansible/infra/403_service_probe_registration.yml b/ansible/infra/403_service_probe_registration.yml index 1a53a63..f6e6bf4 100644 --- a/ansible/infra/403_service_probe_registration.yml +++ b/ansible/infra/403_service_probe_registration.yml @@ -45,16 +45,8 @@ - {name: mempool-mariadb, host: mempool_box_local} - {name: mempool-backend, host: mempool_box_local} - {name: mempool-frontend, host: mempool_box_local} - # The two Monero probes push ONCE A DAY, not every 5 minutes, so they - # need a window wider than a day. 26h, not 24h: the window must exceed - # the push interval or a check that reports exactly on time races its own - # deadline and alerts on jitter. The timers are OnCalendar 05:00 and - # 05:30 with RandomizedDelaySec=900, so the true worst case is ~24h15m. - # Cadence lives in roles/{monero,xmrblocks}/defaults/main.yml; the two - # are one decision and must move together. - - {name: monerod, host: monerod_box_local, heartbeat: '26h'} - - {name: xmrblocks, host: monerod_box_local, heartbeat: '26h'} - - {name: navidrome, host: music_box_local} + - {name: monerod, host: monerod_box_local} + - {name: xmrblocks, host: monerod_box_local} tasks: - name: Build the probe endpoint list @@ -63,7 +55,7 @@ 'name': item.name, 'group': 'probe', 'token': gatus_push_tokens[item.host], - 'heartbeat': item.heartbeat | default('11m')}] }}" + 'heartbeat': '11m'}] }}" loop: "{{ probes }}" - name: Register the probe endpoints diff --git a/ansible/inventory.ini b/ansible/inventory.ini index 9489607..f14deb4 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -16,11 +16,6 @@ forgejo_runner_local ansible_host=forgejo-runner-box lan_ip=192.168.1.132 ansibl arbret_staging_local ansible_host=arbret-staging-box lan_ip=192.168.1.147 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua small_backups_local ansible_host=small-backups-box lan_ip=192.168.1.131 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua monerod_box_local ansible_host=monerod-box lan_ip=192.168.1.146 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -# lan_ip read off nodito's ARP table for the VM's own MAC (bc:24:11:89:fc:1f), -# not guessed. Note .149 previously belonged to another machine - its stale host -# key was still in known_hosts and SSH refused the connection, which is the -# designed behaviour described in group_vars/nodito_vms.yml. -music_box_local ansible_host=music-box lan_ip=192.168.1.149 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua # Local connection to laptop: this assumes you're running ansible commands from your personal laptop [lapy] @@ -60,9 +55,6 @@ monerod_box_local [memos] memos_box_local -[music] -music_box_local - [ci_runner] forgejo_runner_local diff --git a/ansible/roles/monero/defaults/main.yml b/ansible/roles/monero/defaults/main.yml index 3d925ed..633f3c0 100644 --- a/ansible/roles/monero/defaults/main.yml +++ b/ansible/roles/monero/defaults/main.yml @@ -94,16 +94,3 @@ healthcheck_push_url: "" # Bearer token for the Gatus external endpoint. Required whenever a push URL # is set: Gatus rejects an unauthenticated push with 401. healthcheck_push_token: "" - -# WHEN TO CHECK. Daily, at a fixed hour, rather than an OnUnitActiveSec -# interval that drifts with every restart. -# -# Prefixed because the generic `healthcheck` role owns the unprefixed -# healthcheck_on_calendar; an unprefixed default here would shadow it for any -# play that includes both. -# -# Paired with the 26h Gatus heartbeat in -# infra/403_service_probe_registration.yml. Change one and you must change the -# other: the window has to stay above the interval or the check alerts on its -# own jitter. -monero_healthcheck_on_calendar: "*-*-* 05:00:00" diff --git a/ansible/roles/monero/templates/healthcheck.timer.j2 b/ansible/roles/monero/templates/healthcheck.timer.j2 index c088af5..8c850a9 100644 --- a/ansible/roles/monero/templates/healthcheck.timer.j2 +++ b/ansible/roles/monero/templates/healthcheck.timer.j2 @@ -8,17 +8,7 @@ Description=Monero Node Health Check Timer [Timer] OnBootSec=2min -# ONCE A DAY, not every 5 minutes. -# -# A 5-minute poll of monerod buys nothing a daily one does not: the node is a -# multi-week initial sync on a rotating disk, and every failing poll is another -# Signal message. The cadence and the Gatus heartbeat window are ONE decision - -# the window MUST exceed this interval, and it is set to 26h in -# infra/403_service_probe_registration.yml. A 24h push under a 24h window races -# itself and alerts on jitter alone. -OnCalendar={{ monero_healthcheck_on_calendar }} -# Spread against the other daily pushes leaving this box. -RandomizedDelaySec=900 +OnUnitActiveSec=5min Persistent=true [Install] diff --git a/ansible/roles/navidrome/README.md b/ansible/roles/navidrome/README.md deleted file mode 100644 index 0fd5199..0000000 --- a/ansible/roles/navidrome/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# navidrome - -Navidrome — a music server — plus [beets](https://beets.io), the CLI that fixes -tags before Navidrome ever indexes them. Both live on `music-box`. - -Navidrome is a single Go binary with a SQLite database and no external -dependencies beyond `ffmpeg` for transcoding. It speaks two protocols on one -port: its own web UI, and the **Subsonic API** (`/rest/*`), which is what every -mobile client in the ecosystem — Symfonium, Amperfy, Tempo, substreamer — -actually talks to. - -| | | -|---|---| -| Host | `music-box` (nodito VM 8200, 1 core / 1 GB / 50 GB) | -| Port | 4533 | -| Public at | `music.contrapeso.xyz`, proxied by Caddy on `vipy` | -| Library | `/srv/music` — beets-managed, Navidrome reads only | -| Staging | `/srv/incoming` — unsorted, Navidrome never looks here | -| State | `/var/lib/navidrome` — SQLite, cache, transcode scratch | -| Backups | none, deliberately — see below | - -## Exposure - -Public, authenticated by **Navidrome's own login**. There is no `basic_auth` in -front of it, and that is a decision rather than an omission. - -The reason is the Subsonic API. A Subsonic client authenticates with -`?u=&t=&s=` in the query string and has nowhere -to put a second set of credentials. HTTP Basic Auth in front of the whole site -is invisible to most of them, so the choice is between a proxy credential and a -working phone. This estate has made the same call before, for the same reason: -`monerod`'s restricted RPC is proxied bare so Feather can reach it, and its own -Digest auth does the authenticating. - -The cost is stated plainly: Navidrome's login page and the whole `/rest/*` -surface are reachable by anyone who finds the hostname. Navidrome has **no rate -limiting and no 2FA**. The fail2ban jail below covers part of that gap. - -### The fail2ban jail on `vipy`, and what it does not cover - -Installed by the edge play in `services/navidrome/deploy_navidrome_playbook.yml`, -reading a per-vhost Caddy access log at `/var/log/caddy/music.log`. - -It catches **web UI brute force**: a bad login on `/auth/login` returns HTTP -401, which lands in the access log and trips the filter. - -It very likely does **not** catch Subsonic API brute force, because the -Subsonic protocol answers a bad password with **HTTP 200** and puts the failure -in the body (``). -Caddy never sees response bodies, so a status-code filter is blind to it. - -The obvious fix does not work either. Navidrome's own journal logs those -failures, but the request arrives from `vipy` over the tailnet, so the only -address fail2ban could ban on this box is `vipy`'s — which would take down every -service behind the edge. Banning has to happen at the edge; the edge cannot see -the failure. - -So: `/rest/*` is protected by password strength alone. That is the reason the -Navidrome password must be long, random, and used nowhere else. - -To re-check whether this is still true after a Navidrome upgrade: - -```bash -curl -s -o /dev/null -w '%{http_code}\n' \ - 'https://music.contrapeso.xyz/rest/ping.view?u=x&p=wrong&v=1.16.1&c=test' -``` - -`200` means the caveat holds. `401` means it no longer does — widen the jail -and delete this section. - -## Passwords are stored reversibly - -Navidrome **cannot** hash user passwords one-way. The Subsonic API requires the -server to compute `md5(password + salt)` on every request, so it must be able to -recover the plaintext. It encrypts passwords at rest with -`PasswordEncryptionKey` instead (`navidrome_password_encryption_key`, from -`group_vars/all/vault.yml`). - -Two consequences, both real: - -- Root on this box means every Navidrome password in plaintext. **Use a password - unique to this service.** -- **Changing the key invalidates every stored password.** Set it before the first - account exists and leave it alone. - -## Backups: none, deliberately - -Recorded as a decision so it does not read as an oversight — the same treatment -`phoenixd`, `monero` and `xmrblocks` get. - -The music files are large and would swamp the age-encrypted nightly path that -`backup_source` uses. `navidrome.db` is mostly a rebuildable index: delete it and -a rescan reconstructs the library. - -What is **not** rebuildable is the curation on top — playlists, play counts, -ratings, starred items, user accounts. Today there is none of it, which is why -this is a fair trade. **If that changes, this is the decision to revisit**, and -the fix is small: `navidrome.db` is a single SQLite file and would fit the -`backup_source` pattern in about fifteen lines. - -The beets database (`/var/lib/beets/library.db`) is in the same position — it -can be rebuilt from the tags beets itself wrote into the files. - -## beets - -Installed and configured by this role; **not** a service, and deliberately so. -`beet import` is interactive and it *moves and renames files*. A timer that -reorganises a music library unattended is a way to lose one. - -Config at `~counterweight/.config/beets/config.yaml`, library DB in -`/var/lib/beets`. - -```bash -# the usual loop -rsync -avP ~/Music/newstuff/ music-box:/srv/incoming/ -ssh music-box -beet import /srv/incoming -``` - -Navidrome picks up the result on its next scan. - -The single most important line in that config is -`paths.default: $albumartist/...`. Organising by `$artist` instead scatters -every compilation and every "feat." track into a phantom artist, which is how a -library ends up unusable and Navidrome gets blamed for it. - -ReplayGain and lyrics are set `auto: no` — on one core they turn a two-minute -import into a twenty-minute one. Run `beet replaygain` and `beet lyrics` -afterwards, separately. - -### Ownership, which is the fiddly part - -`/srv/music` is `counterweight:navidrome` mode **2750**. The setgid bit is -load-bearing: without it, files beets creates land in the curator's own group, -Navidrome cannot read them, and the symptom is an **empty library rather than a -permission error**. - -## Monitoring - -| Layer | What it answers | Where | -|---|---|---| -| `infra/400` | is the host alive, is the disk filling | automatic, needs `gatus_push_tokens.music_box_local` in the vault | -| `infra/401` | is the `navidrome` unit active | `monitored_services` in `host_vars/music_box_local/main.yml` | -| `infra/403` + this role | does Navidrome actually serve its own UI (`/app/`) | `probe_navidrome` | -| `infra/402` | does `music.contrapeso.xyz` answer from outside, is the cert valid | `public_sites` | - -The role's own check fetches `/app/` and greps for `navidrome`. A unit can be -perfectly `active` while serving nothing useful. - -`/app/` was chosen after testing all three candidates against 0.64.0: -`/ping` returns a bare `.` (proves only that something listens); -`/rest/ping.view` identifies the server but logs two warnings per call, -~576 a day; `/app/` returns `Navidrome` and logs nothing. - -## Upgrading - -```bash -curl -sL https://github.com/navidrome/navidrome/releases/download/v/navidrome_checksums.txt \ - | grep linux_amd64.tar.gz -``` - -Bump `navidrome_version` and `navidrome_sha256` **together** in -`defaults/main.yml`, then re-run the playbook. The install is version-gated, so -an unchanged version is a no-op. - -## Sizing - -1 core / 1 GB / 50 GB. Two things to know: - -- **50 GB is the library ceiling**, not just the OS — `/srv/music` is on the root - disk. Roughly 300–450 albums in FLAC. Growing it means a Proxmox resize plus - `growpart`/`resize2fs` (see `03_vm_disk_enlargement.md`), because - `ignore_changes = all` in the tofu config means tofu will not do it. -- **1 GB is fine for serving, tight for scanning.** The initial scan of a large - library is the memory peak. If it gets OOM-killed, do what `monerod-box` - needed: raise it in the Proxmox UI and record it back in `vms.auto.tfvars`. - -Transcoding is the CPU cost, and it is avoidable: clients that direct-play -(Symfonium, Amperfy) make the server a file shipper. Roku and browsers transcode. diff --git a/ansible/roles/navidrome/defaults/main.yml b/ansible/roles/navidrome/defaults/main.yml deleted file mode 100644 index e328804..0000000 --- a/ansible/roles/navidrome/defaults/main.yml +++ /dev/null @@ -1,85 +0,0 @@ -# Navidrome Configuration Variables -# https://www.navidrome.org/docs/usage/configuration-options/ - -# --- Version ---------------------------------------------------------------- -# Bump navidrome_version and navidrome_sha256 TOGETHER. The hash is the one -# for the linux_amd64 tarball in that release's navidrome_checksums.txt: -# curl -sL https://github.com/navidrome/navidrome/releases/download/v/navidrome_checksums.txt \ -# | grep linux_amd64.tar.gz -navidrome_version: "0.64.0" -navidrome_arch: "linux_amd64" -navidrome_sha256: "efd94d11253234035b5d3cf284da917111736e72daeeda4f45a58251c60553ac" -navidrome_url: "https://github.com/navidrome/navidrome/releases/download/v{{ navidrome_version }}/navidrome_{{ navidrome_version }}_{{ navidrome_arch }}.tar.gz" - -# --- Service user ----------------------------------------------------------- -navidrome_user: navidrome -navidrome_group: navidrome - -# --- Paths ------------------------------------------------------------------ -navidrome_bin_path: /usr/local/bin/navidrome -navidrome_config_dir: /etc/navidrome -navidrome_config_file: "{{ navidrome_config_dir }}/navidrome.toml" -# SQLite database, cache, transcoding scratch. The only thing Navidrome writes. -navidrome_data_dir: /var/lib/navidrome - -# The canonical library. Navidrome READS this and never writes it, which is why -# it is absent from ReadWritePaths in the unit. beets is what writes here. -navidrome_music_dir: /srv/music -# Staging area. Whatever lands here is unsorted and has NOT been through beets; -# Navidrome never looks at it. -navidrome_incoming_dir: /srv/incoming - -# The human who curates the library with beets. Owns both directories above; -# navidrome only needs group read. -navidrome_curator_user: "{{ new_user }}" - -# --- HTTP ------------------------------------------------------------------- -# 0.0.0.0, not loopback: the edge host reaches this across the tailnet. UFW -# denies the port from the LAN and the internet, and tailnet traffic bypasses -# UFW entirely (-A INPUT -j ts-input sits ahead of every ufw-* chain), so -# binding wide here does not widen exposure. Confirmed on mempool-box. -navidrome_bind_address: "0.0.0.0" -navidrome_port: 4533 -navidrome_log_level: info - -# Navidrome's telemetry. Off. -navidrome_enable_insights: false - -# --- Secrets ---------------------------------------------------------------- -# Navidrome CANNOT store user passwords as a one-way hash: the Subsonic API -# authenticates with md5(password + salt), so the server has to be able to -# recover the plaintext. It encrypts them at rest with this key instead. -# -# Consequences, both real: -# - root on this box means every Navidrome password in plaintext. Use a -# password unique to this service and nowhere else. -# - CHANGING THIS KEY INVALIDATES EVERY STORED PASSWORD. Set it before the -# first account exists and then leave it alone. -# Injected by the playbook from group_vars/all/vault.yml; empty here so the -# role stays self-contained and asserts loudly rather than starting insecure. -navidrome_password_encryption_key: "" - -# Set true to see the rendered config in --diff. It carries the key above, so -# the template is diff-suppressed by default. -navidrome_reveal_config: false - -# --- beets ------------------------------------------------------------------ -# beets is NOT a service. It is an interactive CLI the curator runs over SSH to -# fix tags before Navidrome ever indexes them; there is no unit and no timer. -navidrome_beets_enabled: true -navidrome_beets_dir: /var/lib/beets -navidrome_beets_config: "{{ '/home/' ~ navidrome_curator_user ~ '/.config/beets/config.yaml' }}" - -# --- Health check ----------------------------------------------------------- -# "Does Navidrome actually answer", as opposed to infra/401's "is the unit -# active". The exit code is the answer and systemd keeps it: -# systemctl is-failed navidrome-healthcheck.service -navidrome_healthcheck_script_path: /usr/local/bin/navidrome-healthcheck-push.sh -navidrome_healthcheck_service_name: navidrome-healthcheck - -# WHERE TO REPORT HEALTH — the one place to plug in monitoring. -# Empty means check, log, exit honestly, report nowhere. -healthcheck_push_url: "" -# Bearer token for the Gatus external endpoint. Required whenever a push URL -# is set: Gatus rejects an unauthenticated push with 401. -healthcheck_push_token: "" diff --git a/ansible/roles/navidrome/handlers/main.yml b/ansible/roles/navidrome/handlers/main.yml deleted file mode 100644 index acaaf7e..0000000 --- a/ansible/roles/navidrome/handlers/main.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -# NO `when:` on either of these, ever. The uptime_kuma_enabled banner was once -# applied to contiguous blocks and took Fulcrum's entire restart handler with -# it: config changes landed on disk and silently never took effect for months. -# A handler that can be gated off is a handler that will be. -- name: Restart navidrome - systemd: - name: navidrome - state: restarted - daemon_reload: yes - -- name: Restart navidrome health check timer - systemd: - name: navidrome-healthcheck.timer - state: restarted - daemon_reload: yes diff --git a/ansible/roles/navidrome/tasks/beets.yml b/ansible/roles/navidrome/tasks/beets.yml deleted file mode 100644 index ab9d395..0000000 --- a/ansible/roles/navidrome/tasks/beets.yml +++ /dev/null @@ -1,70 +0,0 @@ ---- -# beets: the tag fixer that runs BEFORE Navidrome sees anything. -# -# Navidrome has no metadata fetcher and no tag editor by design - it trusts the -# tags on disk completely. That is the right call, but it means a library with -# bad tags renders as a bad library and there is nothing in Navidrome to fix it -# with. beets is the fix, and it belongs on this box because that is where the -# files are. -# -# There is no unit and no timer here on purpose: `beet import` is interactive, -# it MOVES and RENAMES files, and a cron job that reorganises a music library -# unattended is a way to lose one. - -- name: Install beets and its fingerprinting dependencies - apt: - name: - - beets - # fpcalc. Without it the chroma plugin cannot acoustically identify a - # file whose tags are too mangled to match, which is exactly the case - # beets is most useful for. - - libchromaprint-tools - - python3-acoustid - # lastgenre imports pylast, and Debian's beets package does NOT depend on - # it. Without this the plugin fails to load and every single `beet` - # invocation prints a ModuleNotFoundError traceback while genres are - # silently never fetched - configured, listed in `plugins:`, and inert. - - python3-pylast - state: present - update_cache: true - when: navidrome_beets_enabled | bool - -- name: Create the beets state directory - ansible.builtin.file: - path: "{{ navidrome_beets_dir }}" - state: directory - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_curator_user }}" - mode: '0750' - when: navidrome_beets_enabled | bool - -# Both levels explicitly, parent first. The file module creates missing parents -# like `mkdir -p`, but it creates them owned by ROOT - which would leave -# ~/.config root-owned on a fresh cloud-init box and break every other -# user-level tool that wants to write there later. -- name: Create the curator's XDG config directory - ansible.builtin.file: - path: "{{ navidrome_beets_config | dirname | dirname }}" - state: directory - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_curator_user }}" - mode: '0755' - when: navidrome_beets_enabled | bool - -- name: Create the beets config directory - ansible.builtin.file: - path: "{{ navidrome_beets_config | dirname }}" - state: directory - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_curator_user }}" - mode: '0755' - when: navidrome_beets_enabled | bool - -- name: Write the beets configuration - ansible.builtin.template: - src: beets-config.yaml.j2 - dest: "{{ navidrome_beets_config }}" - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_curator_user }}" - mode: '0644' - when: navidrome_beets_enabled | bool diff --git a/ansible/roles/navidrome/tasks/healthcheck.yml b/ansible/roles/navidrome/tasks/healthcheck.yml deleted file mode 100644 index c263c43..0000000 --- a/ansible/roles/navidrome/tasks/healthcheck.yml +++ /dev/null @@ -1,48 +0,0 @@ ---- -# "Does Navidrome actually answer", as opposed to infra/401's "is the unit -# active". Navidrome can be perfectly `active` and serving 500s - a music -# folder it cannot read does exactly that, and presents as an empty library. - -- name: Create the navidrome health check script - ansible.builtin.template: - src: healthcheck.sh.j2 - dest: "{{ navidrome_healthcheck_script_path }}" - owner: root - group: root - mode: '0755' - validate: "bash -n %s" - -# 0600: carries the Gatus bearer token. -- name: Create the navidrome health check systemd service - ansible.builtin.template: - src: healthcheck.service.j2 - dest: "/etc/systemd/system/{{ navidrome_healthcheck_service_name }}.service" - owner: root - group: root - mode: '0600' - notify: Restart navidrome health check timer - -- name: Create the navidrome health check systemd timer - ansible.builtin.template: - src: healthcheck.timer.j2 - dest: "/etc/systemd/system/{{ navidrome_healthcheck_service_name }}.timer" - owner: root - group: root - mode: '0644' - notify: Restart navidrome health check timer - -- name: Reload systemd daemon after health check units - systemd: - daemon_reload: yes - -# Ungated: enabling a timer is deployment, not monitoring. -- name: Enable and restart the navidrome health check timer - systemd: - name: "{{ navidrome_healthcheck_service_name }}.timer" - enabled: yes - state: restarted - daemon_reload: yes - -- name: Run the navidrome health check once to arm the timer - ansible.builtin.command: "systemctl start {{ navidrome_healthcheck_service_name }}.service" - changed_when: false diff --git a/ansible/roles/navidrome/tasks/install.yml b/ansible/roles/navidrome/tasks/install.yml deleted file mode 100644 index 72adfeb..0000000 --- a/ansible/roles/navidrome/tasks/install.yml +++ /dev/null @@ -1,134 +0,0 @@ ---- -# The user, the directories, and the binary. - -- name: Install Navidrome's runtime dependencies - apt: - name: - # Navidrome shells out to ffmpeg for on-the-fly transcoding. Without it - # the server starts fine and every format the client cannot play natively - # simply fails, which looks like a client bug. - - ffmpeg - - curl - state: present - update_cache: true - -- name: Create the navidrome system group - group: - name: "{{ navidrome_group }}" - system: yes - state: present - -- name: Create the navidrome system user - user: - name: "{{ navidrome_user }}" - group: "{{ navidrome_group }}" - system: yes - shell: /usr/sbin/nologin - home: "{{ navidrome_data_dir }}" - create_home: no - comment: "Navidrome Music Server" - state: present - -- name: Create the navidrome data directory - ansible.builtin.file: - path: "{{ navidrome_data_dir }}" - state: directory - owner: "{{ navidrome_user }}" - group: "{{ navidrome_group }}" - mode: '0750' - -- name: Create the navidrome config directory - ansible.builtin.file: - path: "{{ navidrome_config_dir }}" - state: directory - owner: root - group: "{{ navidrome_group }}" - mode: '0750' - -# Two principals share this tree and neither is root: the curator writes it -# through beets, Navidrome only reads it. setgid (the 2) is load-bearing - -# without it, files beets creates land in the curator's own group and Navidrome -# cannot read them, which presents as an empty library rather than an error. -- name: Create the music library directory - ansible.builtin.file: - path: "{{ navidrome_music_dir }}" - state: directory - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_group }}" - mode: '2750' - -- name: Create the incoming staging directory - ansible.builtin.file: - path: "{{ navidrome_incoming_dir }}" - state: directory - owner: "{{ navidrome_curator_user }}" - group: "{{ navidrome_curator_user }}" - mode: '0755' - -# check_mode: no on the next two - this repo verifies with `--check --diff`, so -# the role has to survive a dry run. A read-only version probe and a temp -# directory are both harmless to perform for real, and without them every task -# below evaluates against an undefined navidrome_tmp.path and the check aborts. -- name: Check the installed navidrome version - ansible.builtin.command: "{{ navidrome_bin_path }} --version" - register: navidrome_installed_version - changed_when: false - failed_when: false - check_mode: no - -- name: Decide whether navidrome needs installing - ansible.builtin.set_fact: - navidrome_needs_install: "{{ navidrome_version not in (navidrome_installed_version.stdout | default('')) }}" - -- name: Create a temporary directory for the navidrome download - ansible.builtin.tempfile: - state: directory - suffix: navidrome - register: navidrome_tmp - when: navidrome_needs_install | bool - check_mode: no - -# checksum: is the verification. Upstream publishes navidrome_checksums.txt per -# release but signs nothing, so there is no signature to check the way -# roles/monero checks binaryfate's - this pins the exact artefact and no more. -- name: Download the navidrome release tarball - ansible.builtin.get_url: - url: "{{ navidrome_url }}" - dest: "{{ navidrome_tmp.path }}/navidrome.tar.gz" - checksum: "sha256:{{ navidrome_sha256 }}" - mode: '0644' - when: navidrome_needs_install | bool and not ansible_check_mode - -- name: Unpack the navidrome tarball - ansible.builtin.unarchive: - src: "{{ navidrome_tmp.path }}/navidrome.tar.gz" - dest: "{{ navidrome_tmp.path }}" - remote_src: yes - when: navidrome_needs_install | bool and not ansible_check_mode - -- name: Install the navidrome binary - ansible.builtin.copy: - src: "{{ navidrome_tmp.path }}/navidrome" - dest: "{{ navidrome_bin_path }}" - remote_src: yes - owner: root - group: root - mode: '0755' - when: navidrome_needs_install | bool and not ansible_check_mode - notify: Restart navidrome - -- name: Remove the navidrome download directory - ansible.builtin.file: - path: "{{ navidrome_tmp.path }}" - state: absent - when: navidrome_needs_install | bool - check_mode: no - -# Check mode skips the three tasks above, so without this a dry run is silent -# about the single most important thing it would do. -- name: Report what a real run would install - ansible.builtin.debug: - msg: >- - Would install navidrome {{ navidrome_version }} to {{ navidrome_bin_path }} - (currently: {{ navidrome_installed_version.stdout | default('not installed') | trim }}) - when: navidrome_needs_install | bool and ansible_check_mode diff --git a/ansible/roles/navidrome/tasks/main.yml b/ansible/roles/navidrome/tasks/main.yml deleted file mode 100644 index 70494a9..0000000 --- a/ansible/roles/navidrome/tasks/main.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# import_tasks, not include_tasks: static imports stay visible to --list-tasks, -# which is the main verification tool in this repo. -- ansible.builtin.import_tasks: install.yml -- ansible.builtin.import_tasks: beets.yml -- ansible.builtin.import_tasks: service.yml -- ansible.builtin.import_tasks: healthcheck.yml diff --git a/ansible/roles/navidrome/tasks/service.yml b/ansible/roles/navidrome/tasks/service.yml deleted file mode 100644 index 92b0aef..0000000 --- a/ansible/roles/navidrome/tasks/service.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- -# Config, unit, start, prove it is up. - -# The key encrypts every stored password. Starting without it would come up -# fine and quietly use a built-in default, so this fails the run instead. -- name: Assert the password encryption key arrived from the vault - ansible.builtin.assert: - that: - - navidrome_password_encryption_key | length > 0 - fail_msg: >- - navidrome_password_encryption_key is empty. It is injected by - services/navidrome/deploy_navidrome_playbook.yml from - group_vars/all/vault.yml. Navidrome stores user passwords REVERSIBLY - (the Subsonic API needs the plaintext to compute md5(password+salt)), - and this key is what encrypts them at rest. Set it before the first - account exists - changing it later invalidates every stored password. - quiet: yes - -- name: Write the navidrome configuration - ansible.builtin.template: - src: navidrome.toml.j2 - dest: "{{ navidrome_config_file }}" - owner: root - group: "{{ navidrome_group }}" - mode: '0640' - diff: "{{ navidrome_reveal_config | default(false) | bool }}" - notify: Restart navidrome - -- name: Create the navidrome systemd unit - ansible.builtin.template: - src: navidrome.service.j2 - dest: /etc/systemd/system/navidrome.service - owner: root - group: root - mode: '0644' - notify: Restart navidrome - -- name: Reload systemd daemon - systemd: - daemon_reload: yes - -- name: Enable and start navidrome - systemd: - name: navidrome - enabled: yes - state: started - -- name: Flush handlers so navidrome is running before we probe it - meta: flush_handlers - -- name: Wait for the navidrome HTTP port - ansible.builtin.wait_for: - port: "{{ navidrome_port }}" - host: 127.0.0.1 - delay: 5 - timeout: 300 - ignore_errors: yes - -- name: Check the navidrome service status - systemd: - name: navidrome - register: navidrome_status - changed_when: false - -- name: Display the navidrome service status - ansible.builtin.debug: - msg: "navidrome is {{ 'running' if navidrome_status.status.ActiveState == 'active' else 'NOT running' }}" diff --git a/ansible/roles/navidrome/templates/beets-config.yaml.j2 b/ansible/roles/navidrome/templates/beets-config.yaml.j2 deleted file mode 100644 index 9620a7d..0000000 --- a/ansible/roles/navidrome/templates/beets-config.yaml.j2 +++ /dev/null @@ -1,81 +0,0 @@ -# beets configuration — managed by Ansible (roles/navidrome) -# https://beets.readthedocs.io/ -# -# Run it by hand, over SSH, when material has accumulated in {{ navidrome_incoming_dir }}: -# beet import {{ navidrome_incoming_dir }} -# -# It is interactive on purpose. `beet import -q` auto-applies only strong -# matches and leaves the rest where they are, which makes a usable review queue -# if the batches ever get boring. - -directory: {{ navidrome_music_dir }} -library: {{ navidrome_beets_dir }}/library.db - -# $albumartist, NOT $artist. This is the single most important line in the -# file: organising by $artist scatters every compilation and every "feat." -# track into its own phantom artist, which is how a library ends up with four -# hundred one-album artists and Navidrome gets blamed for it. -paths: - default: $albumartist/$album%aunique{}/$track $title - singleton: Non-Album/$artist/$title - comp: Compilations/$album%aunique{}/$track $title - albumtype:soundtrack: Soundtracks/$album/$track $title - -import: - # MOVES files out of {{ navidrome_incoming_dir }} into the library. Set - # `copy: yes` and `move: no` instead if you would rather keep originals - # until you trust it. - move: yes - write: yes - languages: en es - log: {{ navidrome_beets_dir }}/import.log - -plugins: chroma fetchart embedart scrub duplicates mbsync missing info lastgenre replaygain lyrics permissions - -# THE trap this library is exposed to, and the fix for it. -# -# beets MOVES files into place, and rename(2) preserves the original group and -# mode - so the setgid bit on {{ navidrome_music_dir }} does NOT apply to them -# (it only affects files CREATED there, such as the album directories beets -# makes, which do come out group-{{ navidrome_group }}). A track that arrives -# mode 0600 therefore lands mode 0600 owned by the curator's own group, and -# Navidrome cannot read it. It does not error: the track is simply ABSENT from -# the library, which is indistinguishable from never having imported it. -# -# Measured on this box: a 0600 file was unreadable by the navidrome user; the -# same file at 0644 was fine. So normalise every imported file. -# NO leading zeros. The plugin does int(str(value), 8), and YAML would parse -# `0644` as octal -> decimal 420, which the plugin then re-reads as octal 0o420. -# Verified on the box: `0644` silently left a 0600 file at 0600; `644` fixes it. -permissions: - file: 644 - dir: 755 - -# Acoustic fingerprinting: identifies a file from the audio itself when the -# tags are too mangled to match. Needs fpcalc, installed by the role. -chroma: - auto: yes - -fetchart: - auto: yes -embedart: - auto: yes -# Strip junk and stale tags before writing clean ones. -scrub: - auto: yes - -lastgenre: - auto: yes - source: album - -# auto: no on both of these deliberately. This box has one core; ReplayGain -# analysis and lyrics fetching during an import turn a two-minute job into a -# twenty-minute one. Run them separately, after the import, when nothing is -# waiting on you: -# beet replaygain -# beet lyrics -replaygain: - auto: no - backend: ffmpeg -lyrics: - auto: no diff --git a/ansible/roles/navidrome/templates/healthcheck.service.j2 b/ansible/roles/navidrome/templates/healthcheck.service.j2 deleted file mode 100644 index 7adcbac..0000000 --- a/ansible/roles/navidrome/templates/healthcheck.service.j2 +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Navidrome Health Check -After=network.target navidrome.service - -[Service] -Type=oneshot -User=root -ExecStart={{ navidrome_healthcheck_script_path }} -# Comfortably longer than the script's 30s curl plus its push retries. -TimeoutStartSec=120 -Environment=HEALTHCHECK_PUSH_URL={{ healthcheck_push_url }} -Environment=HEALTHCHECK_PUSH_TOKEN={{ healthcheck_push_token }} -Environment=NAVIDROME_URL=http://127.0.0.1:{{ navidrome_port }} -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/navidrome/templates/healthcheck.sh.j2 b/ansible/roles/navidrome/templates/healthcheck.sh.j2 deleted file mode 100644 index 1c63e6d..0000000 --- a/ansible/roles/navidrome/templates/healthcheck.sh.j2 +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# Navidrome health check — managed by Ansible (roles/navidrome) -# -# The exit code is the answer and systemd keeps it: -# systemctl is-failed {{ navidrome_healthcheck_service_name }}.service -# Reporting anywhere else is optional and generic. -# -# /app/ rather than /ping, established by testing against 0.64.0 on the box: -# -# /ping 200, Content-Type text/plain, body is a single "." - it -# proves something is listening and nothing more. -# /rest/ping.view identifies itself ("type":"navidrome") but logs TWO -# warning lines per call. At one call per 5 minutes that is -# ~576 spurious warnings a day, which is how you train -# yourself to ignore the warning level. -# /app/ 200, 2.8 KB, Navidrome, and SILENT. -# -# So /app/ is the only one that proves it is really Navidrome serving its real -# UI without polluting the journal. Same reasoning as the xmrblocks check, -# which fetches a real page because a missing templates dir serves 200s of -# nothing. - -URL="${NAVIDROME_URL:-http://127.0.0.1:{{ navidrome_port }}}" -PUSH_URL="${HEALTHCHECK_PUSH_URL:-}" -PUSH_TOKEN="${HEALTHCHECK_PUSH_TOKEN:-}" - -report() { - local status=$1 - local msg=$2 - - # No push URL is normal, not an error: the exit code below is still a - # complete answer for anything reading unit state. - [ -n "$PUSH_URL" ] || return 0 - - local encoded_msg="${msg// /%20}" - local _ok=false - [ "${status}" = "up" ] && _ok=true - curl -s --max-time 15 --retry 2 -o /dev/null -X POST \ - -H "Authorization: Bearer ${PUSH_TOKEN}" \ - "${PUSH_URL}?success=${_ok}&error=${encoded_msg}" || true -} - -# 30s: Navidrome answers /ping in milliseconds normally, but this box has one -# core and a library scan will happily eat it. A tight timeout here would -# manufacture false DOWNs every time a scan runs. -body=$(curl -s --max-time 30 -w '\n%{http_code}' "${URL}/app/" 2>/dev/null) -code=$(printf '%s' "$body" | tail -1) - -if [ -z "$code" ] || [ "$code" = "000" ]; then - report "down" "navidrome not responding" - exit 1 -fi - -if [ "$code" != "200" ]; then - report "down" "navidrome HTTP ${code}" - exit 1 -fi - -if ! printf '%s' "$body" | grep -qi "navidrome"; then - report "down" "navidrome served 200 but not its own UI (something else on the port?)" - exit 1 -fi - -report "up" "OK" -exit 0 diff --git a/ansible/roles/navidrome/templates/healthcheck.timer.j2 b/ansible/roles/navidrome/templates/healthcheck.timer.j2 deleted file mode 100644 index bacb7cd..0000000 --- a/ansible/roles/navidrome/templates/healthcheck.timer.j2 +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Navidrome Health Check Timer -# Deliberately NO `Requires=navidrome.service`. Requires on a timer stops the -# timer when the watched unit stops - "if the thing I am watching goes down, -# stop watching it" - leaving nothing to re-arm when it returns. Fulcrum's -# timer carried it and sat active+enabled for seven months without firing. - -[Timer] -OnBootSec=2min -OnUnitActiveSec=5min -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/ansible/roles/navidrome/templates/navidrome.service.j2 b/ansible/roles/navidrome/templates/navidrome.service.j2 deleted file mode 100644 index 17410da..0000000 --- a/ansible/roles/navidrome/templates/navidrome.service.j2 +++ /dev/null @@ -1,33 +0,0 @@ -[Unit] -Description=Navidrome Music Server -Documentation=https://www.navidrome.org/docs/ -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User={{ navidrome_user }} -Group={{ navidrome_group }} -WorkingDirectory={{ navidrome_data_dir }} -ExecStart={{ navidrome_bin_path }} --configfile {{ navidrome_config_file }} -Restart=always -RestartSec=20 -TimeoutStopSec=60 -StandardOutput=journal -StandardError=journal - -# Hardening: Navidrome writes its database and cache, and nothing else. -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=read-only -# {{ navidrome_music_dir }} is deliberately absent: ProtectSystem=strict leaves -# it read-only, which is exactly right. Navidrome indexes the library; beets -# writes it. If Navidrome ever appears to need write access here, something is -# wrong with the request, not with this line. -ReadWritePaths={{ navidrome_data_dir }} - -LimitNOFILE=65535 - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/navidrome/templates/navidrome.toml.j2 b/ansible/roles/navidrome/templates/navidrome.toml.j2 deleted file mode 100644 index 2db0057..0000000 --- a/ansible/roles/navidrome/templates/navidrome.toml.j2 +++ /dev/null @@ -1,24 +0,0 @@ -# Navidrome configuration — managed by Ansible (roles/navidrome) -# https://www.navidrome.org/docs/usage/configuration-options/ -# -# Mode 0640 root:{{ navidrome_group }}: PasswordEncryptionKey is below. - -MusicFolder = "{{ navidrome_music_dir }}" -DataFolder = "{{ navidrome_data_dir }}" - -Address = "{{ navidrome_bind_address }}" -Port = {{ navidrome_port }} - -LogLevel = "{{ navidrome_log_level }}" - -# See the long note in defaults/main.yml. Changing this invalidates every -# stored password. -PasswordEncryptionKey = "{{ navidrome_password_encryption_key }}" - -EnableInsightsCollector = {{ navidrome_enable_insights | bool | lower }} - -# Scan schedule is left at Navidrome's own default deliberately. The option has -# moved between releases (top-level ScanSchedule vs a [Scanner] section), and -# pinning the wrong spelling here is silently ignored rather than rejected - -# you would get the default anyway and believe you had configured something. -# Check `navidrome config` on the box before adding it. diff --git a/ansible/roles/xmrblocks/defaults/main.yml b/ansible/roles/xmrblocks/defaults/main.yml index 8254393..d90cab8 100644 --- a/ansible/roles/xmrblocks/defaults/main.yml +++ b/ansible/roles/xmrblocks/defaults/main.yml @@ -73,9 +73,3 @@ healthcheck_push_url: "" # Bearer token for the Gatus external endpoint. Required whenever a push URL # is set: Gatus rejects an unauthenticated push with 401. healthcheck_push_token: "" - -# WHEN TO CHECK. Daily, 30 minutes after monerod's own check so the two do not -# contend for the same cold LMDB pages. See roles/monero/defaults for why the -# name is prefixed and why this is paired with the 26h heartbeat in -# infra/403_service_probe_registration.yml. -xmrblocks_healthcheck_on_calendar: "*-*-* 05:30:00" diff --git a/ansible/roles/xmrblocks/templates/healthcheck.timer.j2 b/ansible/roles/xmrblocks/templates/healthcheck.timer.j2 index 6edf61d..48713a4 100644 --- a/ansible/roles/xmrblocks/templates/healthcheck.timer.j2 +++ b/ansible/roles/xmrblocks/templates/healthcheck.timer.j2 @@ -7,14 +7,7 @@ Description=Onion Monero Blockchain Explorer Health Check Timer [Timer] OnBootSec=2min -# ONCE A DAY, not every 5 minutes. See the note in roles/monero's timer: the -# explorer follows the node it reads from, and this check costs a full page -# render out of the LMDB (a cold one measured 68 seconds), so polling it hard -# is not free either. The Gatus heartbeat window is 26h, set in -# infra/403_service_probe_registration.yml, and MUST stay above this interval. -OnCalendar={{ xmrblocks_healthcheck_on_calendar }} -# Spread against the other daily pushes leaving this box. -RandomizedDelaySec=900 +OnUnitActiveSec=5min Persistent=true [Install] diff --git a/ansible/services/gatus/deploy_gatus_playbook.yml b/ansible/services/gatus/deploy_gatus_playbook.yml index 94cd325..d6a5ac1 100644 --- a/ansible/services/gatus/deploy_gatus_playbook.yml +++ b/ansible/services/gatus/deploy_gatus_playbook.yml @@ -22,21 +22,6 @@ number: "{{ signal_number }}" recipients: "{{ signal_recipients }}" default-alert: - # ── SIGNAL ALERTING IS OFF ────────────────────────────────────── - # Flip to true to turn the phone back on. Nothing else needs to - # change: this ONE key disables every Signal alert in the estate. - # - # It works because `enabled` is the only alert field no registration - # playbook sets. Gatus merges a provider's default-alert into each - # endpoint's alert for every field the endpoint left unset, so this - # reaches all ~90 of them without editing a single endpoint - and - # without unregistering anything. The dashboard keeps checking and - # keeps showing red; it just stops sending. - # - # Deliberately NOT done by deleting the `signal:` provider block: - # that would strand ~90 endpoints referencing a provider that no - # longer exists, and lose the number and recipients. - enabled: false # Overridden per group by the registration playbooks; these are the # values that apply if a caller sets nothing. failure-threshold: 1 diff --git a/ansible/services/headscale/templates/acl.json.j2 b/ansible/services/headscale/templates/acl.json.j2 index b120114..5860544 100644 --- a/ansible/services/headscale/templates/acl.json.j2 +++ b/ansible/services/headscale/templates/acl.json.j2 @@ -42,7 +42,6 @@ "tag:fulcrum-box:50002", "tag:mempool-box:8080", "tag:memos-box:5230", - "tag:music-box:4533", "tag:monerod-box:18089", "tag:monerod-box:8081", "tag:arbret-staging-box:80", @@ -114,9 +113,6 @@ "tag:monerod-box": [ "{{ headscale_namespace }}@" ], - "tag:music-box": [ - "{{ headscale_namespace }}@" - ], "tag:nodito": [ "{{ headscale_namespace }}@" ], diff --git a/ansible/services/navidrome/deploy_navidrome_playbook.yml b/ansible/services/navidrome/deploy_navidrome_playbook.yml deleted file mode 100644 index 56d792b..0000000 --- a/ansible/services/navidrome/deploy_navidrome_playbook.yml +++ /dev/null @@ -1,163 +0,0 @@ ---- -# Navidrome + beets on music-box, published at music.contrapeso.xyz. -# -# See roles/navidrome/README.md for the exposure and backup decisions. The two -# that matter here: no basic_auth in front (the Subsonic API the mobile clients -# speak has nowhere to carry a second credential), and the fail2ban jail below -# covers the web UI only. -- name: Deploy Navidrome on music-box - hosts: music - become: yes - vars: - # Vault key is navidrome_encryption_key, not the role's own variable name - - # assigning a var to itself is a recursive reference, not an override. - navidrome_password_encryption_key: "{{ navidrome_encryption_key }}" - healthcheck_push_url: "https://{{ subdomains.gatus }}.{{ root_domain }}/api/v1/endpoints/probe_navidrome/external" - healthcheck_push_token: "{{ gatus_push_tokens[inventory_hostname] }}" - roles: - - navidrome - -- name: Publish Navidrome from the edge host - hosts: edge - become: yes - vars: - navidrome_access_log: /var/log/caddy/music.log - tasks: - # The Debian package usually creates this, but `caddy validate` passes with - # the directory missing and the RELOAD is what fails - so the vhost would - # look deployed and serve nothing. Make it explicit. - - name: Ensure the Caddy log directory exists - ansible.builtin.file: - path: "{{ navidrome_access_log | dirname }}" - state: directory - owner: caddy - group: caddy - mode: '0750' - - # The FILE, not just the directory, and this is load-bearing. - # - # `caddy validate` below runs as root and provisions the logging module, - # which CREATES this file root:root 0600. Caddy itself runs as the caddy - # user, so the subsequent reload then dies with: - # loading new config: setting up custom log 'log0': ... - # open /var/log/caddy/music.log: permission denied - # and - the nasty part - `systemctl reload` hangs rather than failing, - # leaving the unit stuck in "reloading" while the OLD config keeps serving. - # So the vhost looks deployed, validate passes, nothing reports an error, - # and the site is simply absent. - # - # Creating it up front with the right owner makes validate's open a no-op. - - name: Ensure the Caddy access log file exists and is owned by caddy - ansible.builtin.file: - path: "{{ navidrome_access_log }}" - state: touch - owner: caddy - group: caddy - mode: '0640' - access_time: preserve - modification_time: preserve - - # caddy_site_body rather than caddy_site_upstream + caddy_site_resolvers: - # this vhost needs a `log` directive so fail2ban has something to read, and - # the role's template has no knob for one. The escape hatch is the - # documented way to do this - gatus and headscale already use it - and it - # leaves the other vhosts untouched. - - name: Expose Navidrome over HTTPS - ansible.builtin.include_role: - name: caddy_site - vars: - caddy_site_name: music - caddy_site_domain: "{{ subdomains.music }}.{{ root_domain }}" - caddy_site_body: | - log { - output file {{ navidrome_access_log }} { - roll_size 10MiB - roll_keep 5 - } - format json - } - reverse_proxy {{ hostvars['music_box_local'].ansible_host }}:{{ hostvars['music_box_local'].navidrome_port }} { - # Tailscale MagicDNS: music-box has no public address. - transport http { - resolvers 100.100.100.100 - } - } - - # The per-site validate cannot catch a conflict between sites. - - name: Validate the whole Caddyfile - ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile - changed_when: false - - # ── fail2ban ──────────────────────────────────────────────────────────── - # The estate's first jail: 02_firewall_and_fail2ban_playbook.yml installs - # fail2ban everywhere but leaves it on stock config. Kept inline here - # because one jail does not justify a role; if a second service ever wants - # one, that is the moment to promote this to roles/fail2ban_jail alongside - # caddy_site and gatus_endpoint. - # - # Scope, stated so it is not mistaken for more than it is: this catches web - # UI brute force (401 on /auth/login). It does NOT catch Subsonic API brute - # force, because Subsonic answers a bad password with HTTP 200 and the error - # in the body. See roles/navidrome/README.md. - - name: Install the navidrome fail2ban filter - ansible.builtin.copy: - dest: /etc/fail2ban/filter.d/navidrome-caddy.conf - owner: root - group: root - mode: '0644' - content: | - # Managed by Ansible - services/navidrome/deploy_navidrome_playbook.yml - # - # The log file is per-vhost, so any 401 in it is a failed Navidrome - # login and needs no path matching. - [Definition] - failregex = "remote_ip":"".*"status":401 - ignoreregex = - notify: Restart fail2ban - - - name: Install the navidrome fail2ban jail - ansible.builtin.copy: - dest: /etc/fail2ban/jail.d/navidrome.conf - owner: root - group: root - mode: '0644' - content: | - # Managed by Ansible - services/navidrome/deploy_navidrome_playbook.yml - [navidrome] - enabled = true - filter = navidrome-caddy - logpath = {{ navidrome_access_log }} - # ufw, not the Debian default iptables-multiport: UFW is what manages - # the firewall on every host here (infra/02_firewall_and_fail2ban). - banaction = ufw - maxretry = 5 - findtime = 10m - bantime = 1h - notify: Restart fail2ban - - - name: Ensure fail2ban is enabled and running - systemd: - name: fail2ban - enabled: yes - state: started - - - name: Flush handlers so the jail is loaded before we assert on it - meta: flush_handlers - - # A jail that silently failed to load is worse than no jail: it reads as - # protection in `ls /etc/fail2ban/jail.d` and bans nothing. - - name: Assert the navidrome jail is actually active - ansible.builtin.command: fail2ban-client status navidrome - register: navidrome_jail_status - changed_when: false - failed_when: navidrome_jail_status.rc != 0 - - - name: Display the navidrome jail status - ansible.builtin.debug: - var: navidrome_jail_status.stdout_lines - - handlers: - - name: Restart fail2ban - systemd: - name: fail2ban - state: restarted diff --git a/ansible/site.yml b/ansible/site.yml index 4dc5680..284633c 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -56,7 +56,6 @@ - import_playbook: services/monero/deploy_monero_playbook.yml - import_playbook: services/xmrblocks/deploy_xmrblocks_playbook.yml - import_playbook: services/memos/deploy_memos_playbook.yml -- import_playbook: services/navidrome/deploy_navidrome_playbook.yml - import_playbook: services/forgejo-runner/deploy_forgejo_runner_playbook.yml - import_playbook: services/phoenixd/deploy_phoenixd_playbook.yml - import_playbook: services/headscale/deploy_headscale_playbook.yml diff --git a/scripts/add-music.sh b/scripts/add-music.sh deleted file mode 100755 index 5bb7b89..0000000 --- a/scripts/add-music.sh +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bash -# -# Add music to music-box: push it, tag it with beets, let Navidrome see it. -# -# WHY THIS SCRIPT EXISTS -# ---------------------- -# The three steps are simple individually and easy to get subtly wrong together: -# -# 1. rsync to /srv/incoming, NOT to /srv/music. Navidrome reads /srv/music and -# beets is the only thing that should ever write there. Dropping files -# straight into the library skips tagging entirely, which is the whole -# point of having beets. -# 2. `beet import` is INTERACTIVE, so the ssh needs a TTY (-t). Without it the -# prompt never renders and the import appears to hang. -# 3. beets must run as counterweight. Under sudo it finds no config (the -# config lives in ~counterweight), silently falls back to its defaults and -# writes to a SECOND library database in root's home - so you end up with -# files in the wrong place and two competing indexes. -# -# WHAT IT DOES NOT DO -# It does not touch your local files. rsync copies; the `move: yes` in the -# beets config only moves things around ON THE BOX, from /srv/incoming into -# /srv/music. Your originals on this machine are never modified or deleted. -# -# Usage: scripts/add-music.sh [path] (prompts for the path if omitted) -# Needs: rsync and ssh here, tailnet up (music-box resolves via MagicDNS) -set -euo pipefail - -MUSIC_HOST="${MUSIC_HOST:-music-box}" -REMOTE_USER="${MUSIC_USER:-counterweight}" -INCOMING="/srv/incoming" -# Pre-filled into the prompt, editable in place. $HOME rather than a literal -# /home/counterweight so the script still behaves if it is ever run as anyone -# else; for the usual user it resolves to exactly /home/counterweight/music. -DEFAULT_SRC="${MUSIC_LOCAL_DIR:-$HOME/music}" -SSH_KEY="${MUSIC_SSH_KEY:-$HOME/.ssh/counterganzua}" -TARGET="${REMOTE_USER}@${MUSIC_HOST}" - -# Colours, but only when we are actually on a terminal. -if [ -t 1 ]; then - B=$'\033[1m'; DIM=$'\033[2m'; R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; N=$'\033[0m' -else - B=""; DIM=""; R=""; G=""; Y=""; N="" -fi - -say() { printf '%s==>%s %s\n' "$B" "$N" "$*"; } -warn() { printf '%s==>%s %s\n' "$Y" "$N" "$*" >&2; } -die() { printf '%sERROR:%s %s\n' "$R" "$N" "$*" >&2; exit 1; } - -# -n is load-bearing: without it ssh reads OUR stdin and swallows the answers -# meant for the read prompts below, so "Continue? [y/N]" gets EOF instead of -# your keypress. Under `set -e` that aborts the script with no message at all. -ssh_run() { ssh -n -i "$SSH_KEY" -o ConnectTimeout=15 "$TARGET" "$@"; } -# -t for anything that talks to a human (beets prompts, sudo password). No -n -# here - these deliberately DO want your terminal. -ssh_tty() { ssh -t -i "$SSH_KEY" -o ConnectTimeout=15 "$TARGET" "$@"; } - -# --- 0. Preflight ------------------------------------------------------------ -command -v rsync >/dev/null || die "rsync is not installed here." -[ -f "$SSH_KEY" ] || die "SSH key not found: $SSH_KEY (override with MUSIC_SSH_KEY=)" - -say "Checking ${MUSIC_HOST} is reachable..." -if ! ssh_run -o BatchMode=yes true 2>/dev/null; then - die "Cannot reach ${TARGET}. - Is the tailnet up? tailscale status | grep ${MUSIC_HOST} - MagicDNS resolving? getent hosts ${MUSIC_HOST}" -fi - -# --- 1. Where is the music? -------------------------------------------------- -SRC="${1:-}" -if [ -z "$SRC" ]; then - printf '\n%sPath to the album or folder to add%s %s(edit or tab-complete)%s\n' "$B" "$N" "$DIM" "$N" - # -e gives readline (tab completion, arrow keys); -i pre-fills the buffer - # with the default so it is already typed and editable in place. - # -i is ignored when stdin is not a terminal, hence the ${SRC:-} fallback: - # piping into this script, or just pressing Enter, still yields the default. - read -e -i "$DEFAULT_SRC" -r -p "> " SRC || true - SRC="${SRC:-$DEFAULT_SRC}" -fi - -# People paste paths with surrounding quotes (drag-and-drop does this) and with -# a trailing slash. Both change rsync's meaning, so normalise before using it. -SRC="${SRC%\"}"; SRC="${SRC#\"}" -SRC="${SRC%\'}"; SRC="${SRC#\'}" -SRC="${SRC/#\~/$HOME}" -while [ "${SRC}" != "/" ] && [ "${SRC: -1}" = "/" ]; do SRC="${SRC%/}"; done - -[ -n "$SRC" ] || die "No path given." -[ "$SRC" != "/" ] && [ "$SRC" != "$HOME" ] || die "Refusing to sync '$SRC' - that is almost certainly not what you meant." -[ -e "$SRC" ] || die "Not found: $SRC" -[ -r "$SRC" ] || die "Not readable: $SRC" - -BASENAME="$(basename -- "$SRC")" -if [ -d "$SRC" ]; then - COUNT=$(find "$SRC" -type f | wc -l | tr -d ' ') - SIZE=$(du -sh "$SRC" 2>/dev/null | cut -f1) - [ "$COUNT" -gt 0 ] || die "$SRC contains no files." -else - COUNT=1 - SIZE=$(du -h "$SRC" 2>/dev/null | cut -f1) -fi - -# --- 2. Does it fit? --------------------------------------------------------- -# /srv/incoming and /srv/music share one filesystem, so the beets move is a -# rename and costs nothing extra - but the transfer itself still needs room. -AVAIL=$(ssh_run "df -Pk ${INCOMING} | awk 'NR==2 {print \$4}'" 2>/dev/null || echo 0) -NEED=$(du -sk "$SRC" 2>/dev/null | cut -f1) -if [ "${AVAIL:-0}" -gt 0 ] && [ "${NEED:-0}" -gt "${AVAIL}" ]; then - die "Not enough space on ${MUSIC_HOST}: need $((NEED/1024)) MB, have $((AVAIL/1024)) MB. - The library shares the 50 GB root disk - see roles/navidrome/README.md." -fi - -printf '\n' -say "About to add:" -printf ' source %s\n' "$SRC" -printf ' files %s (%s)\n' "$COUNT" "${SIZE:-?}" -printf ' lands in %s:%s/%s\n' "$MUSIC_HOST" "$INCOMING" "$BASENAME" -printf ' free %s MB on the box\n\n' "$((${AVAIL:-0}/1024))" -printf '%sYour local files are only read, never moved or deleted.%s\n\n' "$DIM" "$N" - -read -r -p "Continue? [y/N] " REPLY -case "$REPLY" in [yY]*) ;; *) say "Nothing done."; exit 0 ;; esac - -# --- 3. Push ----------------------------------------------------------------- -printf '\n' -say "Pushing to ${MUSIC_HOST}:${INCOMING}/ ..." -# No trailing slash on "$SRC": that copies the DIRECTORY, not its contents, so -# the album arrives as its own folder and beets sees one album per directory. -# --chmod is not cosmetic. `rsync -a` preserves the SOURCE mode, and beets -# moves files into the library with rename(2), which keeps their group and -# mode. A track that leaves this machine mode 0600 therefore arrives in -# /srv/music mode 0600 owned by the curator's group - and Navidrome cannot read -# it. It does not error; the track is simply absent from the library. The beets -# `permissions` plugin also normalises this on the box; this is the same fix -# applied at the door so /srv/incoming is sane too. -rsync -av --info=progress2 --human-readable --chmod=D755,F644 \ - -e "ssh -i $SSH_KEY -o ConnectTimeout=15" \ - "$SRC" "${TARGET}:${INCOMING}/" - -# --- 4. Tag ------------------------------------------------------------------ -printf '\n' -say "Running beets. This is interactive - see the key guide below." -printf '%s A apply M more candidates E enter search I paste MB id%s\n' "$DIM" "$N" -printf '%s U use tags as-is S skip T as tracks G group albums%s\n\n' "$DIM" "$N" - -# As counterweight (NOT sudo) so it finds ~/.config/beets/config.yaml. -# `|| true` because beets exits non-zero when you skip or abort an album, and -# that is a normal outcome, not a script failure. -ssh_tty "beet import ${INCOMING}/$(printf '%q' "$BASENAME")" || true - -# --- 5. What is left behind? ------------------------------------------------- -printf '\n' -LEFT=$(ssh_run "find ${INCOMING} -type f 2>/dev/null | wc -l" | tr -d ' \r') -if [ "${LEFT:-0}" -gt 0 ]; then - warn "${LEFT} file(s) still in ${INCOMING} - skipped, or beets could not match them." - warn "Re-run later with: ssh ${MUSIC_HOST} 'beet import ${INCOMING}'" - warn "Details: ssh ${MUSIC_HOST} 'tail /var/lib/beets/import.log'" -else - printf '%s==>%s %sEverything imported; %s is empty.%s\n' "$B" "$N" "$G" "$INCOMING" "$N" -fi - -# --- 6. Optional post-processing --------------------------------------------- -# replaygain and lyrics are auto:no in the beets config on purpose - on a single -# core they turn a 2 minute import into a 20 minute one. Offered here so they do -# not get forgotten entirely, but off by default. -printf '\n' -read -r -p "Run replaygain now? (slow on 1 core) [y/N] " REPLY -case "$REPLY" in [yY]*) say "Running beet replaygain..."; ssh_tty "beet replaygain" || warn "replaygain reported a problem." ;; esac - -# --- 7. Let Navidrome see it ------------------------------------------------- -printf '\n' -say "Restarting Navidrome so it picks up the new files..." -ssh_tty "sudo systemctl restart navidrome" - -say "Waiting for it to come back..." -for i in $(seq 1 20); do - if ssh_run "curl -sf -m 5 -o /dev/null http://127.0.0.1:4533/app/" 2>/dev/null; then - printf '%s==>%s %sNavidrome is up.%s\n' "$B" "$N" "$G" "$N" - break - fi - [ "$i" -eq 20 ] && warn "Navidrome did not answer within ~60s. Check: ssh ${MUSIC_HOST} 'systemctl status navidrome'" - sleep 3 -done - -# --- 8. Summary -------------------------------------------------------------- -printf '\n' -say "Library now holds:" -ssh_run "beet stats" 2>/dev/null | sed 's/^/ /' || warn "could not read beet stats" -printf '\n%s==>%s %sDone.%s https://music.contrapeso.xyz\n' "$B" "$N" "$G" "$N" diff --git a/tofu/nodito/vms.auto.tfvars b/tofu/nodito/vms.auto.tfvars index 5db24a9..40877bd 100644 --- a/tofu/nodito/vms.auto.tfvars +++ b/tofu/nodito/vms.auto.tfvars @@ -85,23 +85,10 @@ vms = { "arbret-staging-box" = { vmid = 3200 name = "arbret-staging-box" - cores = 2 + cores = 2 memory_mb = 2048 disk_size_gb = 20 ipconfig0 = "ip=dhcp" - }, - # disk_size_gb here is the music library's ceiling, not just the OS: the - # library lives on the root disk at /srv/music. ~45 GB is usable after - # Debian, so roughly 300-450 albums in FLAC. Growing it means a Proxmox - # resize + growpart/resize2fs (03_vm_disk_enlargement.md), because - # ignore_changes = all means tofu will not do it. - "music-box" = { - vmid = 8200 - name = "music-box" - cores = 1 - memory_mb = 1024 - disk_size_gb = 50 - ipconfig0 = "ip=dhcp" } }