From 1e1d7b5d023b83dbab465d656017b393438e1cb4 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Tue, 6 Aug 2024 20:04:26 +0100 Subject: [PATCH 01/37] :wrench: Add URLs for Gitea and Hedgedoc --- .../infrastructure/programs/declarative_sre.py | 2 ++ .../infrastructure/programs/sre/gitea_server.py | 5 ++++- .../infrastructure/programs/sre/hedgedoc_server.py | 5 ++++- .../infrastructure/programs/sre/user_services.py | 4 ++-- .../infrastructure/programs/sre/workspaces.py | 6 ++++++ .../workspace/workspace.cloud_init.mustache.yaml | 10 ++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index 614d8ac2b2..81728d215c 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -347,6 +347,8 @@ def __call__(self) -> None: data_collection_rule_id=monitoring.data_collection_rule_vms.id, data_collection_endpoint_id=monitoring.data_collection_endpoint.id, database_service_admin_password=data.password_database_service_admin, + gitea_hostname=user_services.gitea_server.hostname, + hedgedoc_hostname=user_services.hedgedoc_server.hostname, ldap_group_filter=ldap_group_filter, ldap_group_search_base=ldap_group_search_base, ldap_server_hostname=identity.hostname, diff --git a/data_safe_haven/infrastructure/programs/sre/gitea_server.py b/data_safe_haven/infrastructure/programs/sre/gitea_server.py index 655f9b0016..b69c867c6e 100644 --- a/data_safe_haven/infrastructure/programs/sre/gitea_server.py +++ b/data_safe_haven/infrastructure/programs/sre/gitea_server.py @@ -336,7 +336,7 @@ def __init__( ) # Register the container group in the SRE DNS zone - LocalDnsRecordComponent( + local_dns = LocalDnsRecordComponent( f"{self._name}_gitea_dns_record_set", LocalDnsRecordProps( base_fqdn=props.sre_fqdn, @@ -348,3 +348,6 @@ def __init__( child_opts, ResourceOptions(parent=container_group) ), ) + + # Register outputs + self.hostname = local_dns.hostname diff --git a/data_safe_haven/infrastructure/programs/sre/hedgedoc_server.py b/data_safe_haven/infrastructure/programs/sre/hedgedoc_server.py index 5187401de5..e6564bc991 100644 --- a/data_safe_haven/infrastructure/programs/sre/hedgedoc_server.py +++ b/data_safe_haven/infrastructure/programs/sre/hedgedoc_server.py @@ -315,7 +315,7 @@ def __init__( ) # Register the container group in the SRE DNS zone - LocalDnsRecordComponent( + local_dns = LocalDnsRecordComponent( f"{self._name}_hedgedoc_dns_record_set", LocalDnsRecordProps( base_fqdn=props.sre_fqdn, @@ -327,3 +327,6 @@ def __init__( child_opts, ResourceOptions(parent=container_group) ), ) + + # Register outputs + self.hostname = local_dns.hostname diff --git a/data_safe_haven/infrastructure/programs/sre/user_services.py b/data_safe_haven/infrastructure/programs/sre/user_services.py index a5abed4fe8..5eb04bdfbb 100644 --- a/data_safe_haven/infrastructure/programs/sre/user_services.py +++ b/data_safe_haven/infrastructure/programs/sre/user_services.py @@ -94,7 +94,7 @@ def __init__( child_tags = {"component": "user services"} | (tags if tags else {}) # Deploy the Gitea server - SREGiteaServerComponent( + self.gitea_server = SREGiteaServerComponent( "sre_gitea_server", stack_name, SREGiteaServerProps( @@ -119,7 +119,7 @@ def __init__( ) # Deploy the HedgeDoc server - SREHedgeDocServerComponent( + self.hedgedoc_server = SREHedgeDocServerComponent( "sre_hedgedoc_server", stack_name, SREHedgeDocServerProps( diff --git a/data_safe_haven/infrastructure/programs/sre/workspaces.py b/data_safe_haven/infrastructure/programs/sre/workspaces.py index 954bd06bb1..b48de97668 100644 --- a/data_safe_haven/infrastructure/programs/sre/workspaces.py +++ b/data_safe_haven/infrastructure/programs/sre/workspaces.py @@ -28,6 +28,8 @@ def __init__( data_collection_endpoint_id: Input[str], data_collection_rule_id: Input[str], database_service_admin_password: Input[str], + gitea_hostname: Input[str], + hedgedoc_hostname: Input[str], ldap_group_filter: Input[str], ldap_group_search_base: Input[str], ldap_server_hostname: Input[str], @@ -54,6 +56,8 @@ def __init__( self.data_collection_rule_id = data_collection_rule_id self.data_collection_endpoint_id = data_collection_endpoint_id self.database_service_admin_password = database_service_admin_password + self.gitea_hostname = gitea_hostname + self.hedgedoc_hostname = hedgedoc_hostname self.ldap_group_filter = ldap_group_filter self.ldap_group_search_base = ldap_group_search_base self.ldap_server_hostname = ldap_server_hostname @@ -116,6 +120,8 @@ def __init__( apt_proxy_server_hostname=props.apt_proxy_server_hostname, clamav_mirror_hostname=props.clamav_mirror_hostname, database_service_admin_password=props.database_service_admin_password, + gitea_hostname=props.gitea_hostname, + hedgedoc_hostname=props.hedgedoc_hostname, ldap_group_filter=props.ldap_group_filter, ldap_group_search_base=props.ldap_group_search_base, ldap_server_hostname=props.ldap_server_hostname, diff --git a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml index 7137b94253..408d39dc02 100644 --- a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml +++ b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml @@ -93,6 +93,16 @@ write_files: ansible-playbook /desired_state/desired_state.yaml popd + - path: "/root/gitea.url" + permissions: "0400" + content: | + URL=http://{{gitea_hostname}} + + - path: "/root/hedgedoc.url" + permissions: "0400" + content: | + URL=http://{{hedgedoc_hostname}} + mounts: # Desired state configuration is in a blob container mounted as NFSv3 - ["{{storage_account_data_desired_state_name}}.blob.core.windows.net:/{{storage_account_data_desired_state_name}}/desiredstate", /desired_state, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] From d4593ed7b4525ff4be75e203727d7914ccaf367d Mon Sep 17 00:00:00 2001 From: James Robinson Date: Tue, 6 Aug 2024 21:05:48 +0100 Subject: [PATCH 02/37] :sparkles: Add desktop files and icons for gitea and hedgedoc --- .../workspace/ansible/desired_state.yaml | 28 ++++++++++++++++++ .../files/etc/skel/Desktop/gitea.desktop | 6 ++++ .../files/etc/skel/Desktop/hedgedoc.desktop | 6 ++++ .../ansible/files/usr/share/icons/gitea.png | Bin 0 -> 8841 bytes .../files/usr/share/icons/hedgedoc.png | Bin 0 -> 13773 bytes 5 files changed, 40 insertions(+) create mode 100644 data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop create mode 100644 data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop create mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png create mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index ff5df392a3..b8f91d82c9 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -130,6 +130,7 @@ state: directory mode: '0755' loop: + - /etc/skel/Desktop - /etc/xdg/xfce4/terminal - /etc/xrdp - /usr/local/share/xrdp @@ -200,6 +201,33 @@ - xrdp - xrdp-sesman + - name: Copy icons + ansible.builtin.copy: + src: usr/share/icons/{{ item }} + dest: /usr/share/icons/{{ item }} + mode: '0444' + loop: + - gitea.png + - hedgedoc.png + + - name: Copy desktop files + ansible.builtin.copy: + src: etc/skel/Desktop/{{ item }} + dest: /etc/skel/Desktop/{{ item }} + mode: '0755' + loop: + - gitea.desktop + - hedgedoc.desktop + + - name: Set desktop file URLs + ansible.builtin.lineinfile: + path: /etc/skel/Desktop/{{ item }}.desktop + line: "{{ lookup('file', '/root/{{ item }}.url') }}" + state: present + loop: + - gitea + - hedgedoc + - name: Add polkit rule to allow colord ansible.builtin.copy: src: etc/polkit-1/localauthority/50-local.d/50-colord.pkla diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop new file mode 100644 index 0000000000..1c97fa3361 --- /dev/null +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Version=1.0 +Type=Link +Name=Gitea +Comment= +Icon=/usr/share/icons/gitea.png diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop new file mode 100644 index 0000000000..52594cde0b --- /dev/null +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Version=1.0 +Type=Link +Name=HedgeDoc +Comment= +Icon=/usr/share/icons/hedgedoc.png diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png b/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca49a3918e4960dff2182c03c241ff23e997a8d GIT binary patch literal 8841 zcmeHrcT`i`w{-%cNbev;LO_}XNT>;fE`op{Md=VifKZZ1LKg)EL5hMPy;?w#st5=O zC<>yY^bUd|QlvMfzJT}MpLe`B#{2FV-}~?8jFWxN+Vjl4*4%5aarU`xcIFf_13v=* z0AMyW&@-pBVFxEYE#<#i1O1NDGzD8)lg-fqV4^R<4d;yolY@v@Fg6h91^@&O`Iu>Q zG=u5uS7S7^!zDFZ75xh|r&nXIqz{nY?Gt*r!#9E&$2-Fl!>eT;-s9r3uW|0xyOPgx zBb<)a*l^QG=nU|w9)Fvhn5O33vSPWJ(r~H1Des1rUD| zyFnL$v)^s_&M61c0|3AXoUX2!p|0+qu}~t&3Qa^AG;8r)`&8f=YbiLS{YpgF@39g0 zg<>m7ok+72m9x&iyOH9|hB2{s$8CvA?iI7+pNk(|eFUc83sY57m8vzk{^@um5A4{; zXmiM?ZQo5HaHXfQ(l$p}k&V9iZ7$MJE7(#Dn3kE_;WtqiV z%~rE~4|tOVheR6Q`jx;dh0K%>LAk0grG3MHWCeERF>+}7I$Zb}PZ~sryX(h!yXC0r z$fSf=&!Er2EI;EuduaDcI=y*jKVJXZrbNm5Th2~o{++7F@BK0`255Gp1h9@awN;O^ zh<=@Z%yp)03DnNr8%~f^ol<4L?ybS5_T1Xxf$O01XB)5vY0l9ZUwPWdp(zB^;DpPpx!wojyLk8`mJk z8bJtup;S%CCS_>=Y-X*tXjN4~;%}3mFDx%O6F%*g%&aZag$?-@ce$_kQ*tgCN69T~ z6JsQX;3JE6CAeT^1AT~;qyqrdH3Nxgj2D&+cEP&i@F>V)T_XgHb45YUE1Aff5OuL0 zID=qctVQq{OH8mA2H^_P)L>8#L{bQRuw*ni(8n83LI$EBhrCG2^?_Lq0zQP0y-*Nq z6EmUea0%QXeWC_0R zaxeq}Atw))gTtW|1e6qnC!+(Qc#`M=#V-y$ED7U_Ba(3hJotbU?LzP)qaYAUKlo4o ze269{f6?Pff2cswLoN_al!M92%lY`o{r&`reA1sn@`pqJ;}axH$`X|`$C3zsz8LID ze=MFX`a8ubLld*V7!E>m$N3NsgQAG{x4~R7f8mIJzTSrzSBxCi8|yIzkaDY-$>s&Fh+UO`@71*WVluc)H*8D|#iYpZD=H>!bKwwo=U9o5v1vi)9C`^5El&nO1|L)ZRl`Dlx34=ka zs<^pAv8rw=P(?*o1t>z^RRxMrP=u){E27~pit>k42lIf`HZw#);IhA2%)HTLH-fJZ z3SxrBfK4s_gSEu@U@ge#1Jz(Ea5zE*rl_E(09S=6EBps^4(m&zWZVHMOkP$&^{~ek zgVd)`qA8h!^Fg~~<%oFq!;XV_K~kcjD2qNwR0{K6MQWR1aB1Nz*+DC zc=IyC)SQWn??zwUnBdgBh)5*U2ADv)T*uMkLRf2`}FpA@cNT^J8E-W^LR zAAjWQpLX2;kPC1nB^MVL1vnI=0HdT6T0sG-icpbWn1%$lf-`Pn7H*x^l z7pv_~(UGDRB|i_f0!#jyLaD#Q13a(?^FvV#Dz6It?~2L&5wYAs)%Y`Hb-Dj0iuxho zw=zR{?w5^Hz9`jD?yqwAhiC_d=l}5c$7K8;jzFRQcaVRi@4s^WE7w0#;2(ki&8~mt z`bP@tb|D53ql5-oD2{Sl_YKefJ4zxPuntZXg)hA(9$ZU>GGx(RwNw6 z1fo~Vpt_=^#l}s1LBqwy?ISS4@V$U|)JMoquW&eh=3EH*yT2e!47)|XU5K8RVq)l7 z5J;PbD#9|NJPImvWKg=TZngDU<{W-_?Q-N9y1#-nfi4TZk&nO7sBO9Ja;NNjB?PUMiWJ?k`!fc zzMq9^@#=u%Hh8Q`J=g6Cz;ZPop)gp}e3PFR#2I#>h{a%W0vJ;y^es$har32Z!+E(m zR3#}dgh^Y6p~HbCz<{B9GW~f(&%Nr3`lwgZpbB%2STIjC(3YyC{**oQr$_rG_cxw$rN{1UE(6rb=)~RS5yl&BoIiY*S^#@aZw-Fdblb1aKmN+rQWN7 z(mHN-JdXkV_EuAfMW2mQz70@gIL&c=W}{z*s@QR&IxwW+jc4vzt;$W+eN>BGK*iWc zzQs+Wo2!>^)*65>A!m(_5TCr881q=y{ZdA|$20=)(9V-d)zFPN|5kw7W_Pvl@-&j; zdJ}`$S*x>ROw3+AB1(W)0D`gq5PnSxurXN0C$`U`K3_8FbjwFfHjyE`Qy|(?M^a}^ z#Zlsg+j{=GL&AIN(z8>RoHiZpQ8YF8&jHiESFWcl(R5fS`3=C|*v3v38dw9x9wHgM zZawAaZxl_)=ilemE{p|@Ck{R@)~cP|m=oM+ma@K0>|_JqyU0=VGzV`1ET(IIlRxdWX_bzae0*r+a#ft5kcq@T6oNVjq3!FV>%C+kf&Q#dRq=q zvTJtre$%2oOLg7C84F91uY2;`VQibAhduSOPHN7N+medHH0fWc=Ek$u6igY!{UBMB z_u*L?!lz3d{q~evCc}@m@AD>HHq6YJ`{?{bbOHjLz=F8EHU^b}BZQTRn`}G1U*D`( zb8JMES+I3w^(Kf>=^ArsP^&Ly%=XP)xQm*&i$H&+$?6Kr=j%>sFbF`5{9G@teC{}` z&2FF75%$O=W;bfR@2H7j>l+4h5br6ayqoD)=qsKApSxKF@?AlvR`}~R+YhL`Q&K9t z(-2^oJDbw9P+ks@-2my1GN|XFWwpBbF72G@{X**yP~}AZ*@R2hjEc>FS}W&TB5^u4 zM!#mF)OizBY|C^6hQb{k1d7N!O#`4YbX>sJV)4so$-0Z;VyEtnt9HN!JYU{9N!rgl z&o{I!Zd(<7DizskDm=A~nzMOK5aK%m6=964oDvmhC^Sy*t%F_qUf^65PH!Yr&0NnbaxsKd(OFXNw{TPTMCbje z_3f#Z=0sCbU28y9E^nBwI74-R5LB;|vIK!m>Cfa4te>#gE+@H5s7S7S^_`qU;WdA+1vT*K&`kCWc*HGsUbAN*Wwg@JSze>>YS^UmJbW{{^VKT57HhmD>MkI;Pd zD5#Z6L44lCJZ2-FxO%&NVdJ`i{)RB2HvdMB#;fxo8h|{n9VPq0P$Kh&o)~pm&5fCW zkK==O``1e5p)X|UGv(GIsM4YA?#tP%e9Y>g`oTOqqdu^tSKNS3=+UuNcBFmZYxig7 zrm&z>B75yNb4?i916E(10s-D!WdPpSjHXGRl8Jb5C@Cqpi9A85xqvvLh6h zk|;FYk11~(zjZsglsahYgG$ha_Y(KSM=eNX*iu&Cy*EYp5ul86knj2NHd>m9IQy4! z8FT$k-9C`)p}zT!R1;Z2vZI-Dt3Xe|FM3fWf!nQAN?FO}u2%h**RIxd-_T}^} zCl`;-mz5i#Y06vNjz=4#MI6MHQ{&4w%r{vQf|`&?ywTKCL+-;abK4+}XHjnSj845j zHENHGZ_AutkSbZ|=uRY!jJQDpD1zE$5t-_NuubDrwS zZcT@o*oU?j+n2#LSDzu5a9U4=<2xTVDs!*Tja1R`l(-43lV={MIuHQ192klO6P zQorzxM#vnK6Y5{4yk1W2&z>j79^NwlkTvfJ$xeWqC)X`x&%Al_X;nc$on4K4(_+wW z16KV)_crep6i2qt&)6RpEg%g?wOG=9_uWfX528JSB^-q%f!13J`3)Xn2`hxAfJ=^R zviC+j0*3U1dDGKYX$A2@X~NM?qlLR3x01|_Q$Hma>r*I-BfM{c3^4V`8T!;oF9n#<-(2Eq;lb@W zQ>Tdj)U507Bg8yOwb4%NUmFW^HlEe>$Uq3!KUiaO`X0y{b}liyrl9{#?jSQy`|15- z=M{Dr(v5{+SARUf11QV+I&1%4wUfX0$=gv`U%aSA0G}X}6-b&UH7;+!*n?JPnWnO^ zMe<~!f1yv$j8M6eoZW7`l`al7?rZUd^!FrW>m8n992S`LPcC3Df+|>jslUV=9@*GM|fd3xd-% z&fsI}iSDIyskf|b80obh+i)-0nCzd$wAU2WqWlZzf#WCMLj@nc{%}`d>0{HACaJpa zDt1w+$q;AWTg~)SqCvOC2de@rk9$Z!tpt$Fe`_ykhzU+d!@LciJ97olN zZzFMbPRYr+qL^VOfUPwrU&?2vsi7B1n!SXJ##f}e34VRm6R1;iJ5d2_8`ij=GwF_Vyg@uIiOq=Sr&ZDVRvL`*O51 zAlYg2Evh~?=`{ji-~UB>sAb#Hd%gF@dq#`*Civa{{H6kk=nTsyJMb<{IkuF3qh)bG z`Zf?1l`(sb(df(*d@G#cblfOje?2Hm8QSjt!br;Id9(Z^h+pMi+$y5?;^QUB6Qdur ztXef^hmfDhGMeoJK=0a7+&jP+u#~}eUUhvILv7D5k)UJGxI_Dfv8v_f zd!E#*$O%3-jydT)}OVRwuTSo20dZE6D` z5XQV=qYSLnm+zQcWTgrQref;H?stBwsn~uc&BFWrE=efpGVE+gjoLawV@Q4X&WE=U z$Wav!-|UwbQ&Q-~PQiwDhtT{J!gH7p<2&U~2GrWP0B^$)ZVbve|2tk8b}lj+4)Z9= zoQmi4n=WM$Hg7?SlEc?g$tE$-9J zj7>?muAv{TXE}IL*{yRR$}0LHO>rCjE&ni+!~ke zCW%xDc%rHwtjWlQs^^e$cC;+>Uj>Q|@GC?;-%1~?uG1ZVHuDXD+D101tn5zC9_u_xQ7jqumca&7GR?nA4`uOD!GGh7=GhzL85zLrN$A zxzYf%Y;Bs4|D0nkNv-d8TKzEFR$oh$wj1$Uw%p>4TI{J(V^7K8J-Keef6j~b@r6z4 zn76ESMYE5OyCiB#SYJ5{s0wuiTeKdZ1hT4BW&2irXUp8gF74j@)CGrx`!5R_n;fl< zjuYwYN-S=EBs-ghY-L7wt(*4qQA^66aGjd{Cdq0%PB$J&j@sT%;01&Mzi_b78@vt_ z@w;m=+2;Ok&scfCCdUJs^QzBKgC;*DM4)ZHv>7s-&~b;ZU&7S&b97;J>_ddq)|`;h z;6-Qc@k*za$*T8MjlMEhgGK5_aYirRnB!Np%yr`8FHj%DM9&Ai+__kGS!oPx7qZTs z%Duc&es* z*h#SC;)`@ivwhj?YN@O9d!|%YR!)p-Gj$zoZngb#8spBQ;MM+w_v=wb5xT^YMx`ftl?}Y~>GL1vpZHub z=mYL%tRf)L3vsine4<-pXNG^U@D5U)V%Si?yfzY)9pQ*R)%pCI)76oHQ%xi>(C_YkGW8~H~t>N_w@bcp<&|U5gvoUnhF8UNg4onbVByF`5CIcsHKpKT##E>TJi!hRrxjZ z%a)ibwIrHosU{~Fhp}$9&Nw3cqGtPks^9(!D9<)4Pi0BAWt&-Nd2@zOty*KzA?MS* zi2K63Z}vC3B4ifj07e$FDJz4>saIFSD^rPK^3bC{jKt0Sr%`(JR$)zVcsFbi~I1 literal 0 HcmV?d00001 diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png b/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png new file mode 100644 index 0000000000000000000000000000000000000000..6dae00fd53caf67b3442cfba739c16b22cadddd1 GIT binary patch literal 13773 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 Date: Tue, 6 Aug 2024 21:17:40 +0100 Subject: [PATCH 03/37] :sparkles: Add desktop icons for input, output and shared folders --- .../resources/workspace/ansible/desired_state.yaml | 3 +++ .../workspace/ansible/files/etc/skel/Desktop/input.desktop | 7 +++++++ .../ansible/files/etc/skel/Desktop/output.desktop | 7 +++++++ .../ansible/files/etc/skel/Desktop/shared.desktop | 7 +++++++ 4 files changed, 24 insertions(+) create mode 100644 data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/input.desktop create mode 100644 data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/output.desktop create mode 100644 data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/shared.desktop diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index b8f91d82c9..ed46d000d5 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -218,6 +218,9 @@ loop: - gitea.desktop - hedgedoc.desktop + - input.desktop + - output.desktop + - shared.desktop - name: Set desktop file URLs ansible.builtin.lineinfile: diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/input.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/input.desktop new file mode 100644 index 0000000000..97e64b5b95 --- /dev/null +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/input.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.0 +Type=Link +Name=input +Comment= +Icon=drive-removable-media +URL=/data diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/output.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/output.desktop new file mode 100644 index 0000000000..4dc474784a --- /dev/null +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/output.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.0 +Type=Link +Name=output +Comment= +Icon=drive-removable-media +URL=/output diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/shared.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/shared.desktop new file mode 100644 index 0000000000..3e4e97fde7 --- /dev/null +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/shared.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.0 +Type=Link +Name=shared +Comment= +Icon=drive-removable-media +URL=/shared From 6d0da02be4fbc3dc316cb9eb8033dcd23ba5af3d Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 7 Aug 2024 15:08:58 +0100 Subject: [PATCH 04/37] Move icons to /usr/local/share --- .../workspace/ansible/desired_state.yaml | 12 ++++++------ .../files/etc/skel/Desktop/gitea.desktop | 2 +- .../files/etc/skel/Desktop/hedgedoc.desktop | 2 +- .../ansible/files/usr/share/icons/gitea.png | Bin 8841 -> 0 bytes .../ansible/files/usr/share/icons/hedgedoc.png | Bin 13773 -> 0 bytes 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png delete mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index ed46d000d5..c904a0bcf1 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -27,7 +27,7 @@ tags: apt ansible.builtin.script: executable: /bin/bash - cmd: /desired_state/install_deb.sh "{{ item.source }}" "{{ item.filename }}" "{{ item.sha256 }}" + cmd: "/desired_state/install_deb.sh {{ item.source }} {{ item.filename }} {{ item.sha256 }}" creates: "{{ item.creates }}" loop: "{{ deb_packages[ansible_facts.distribution_release] }}" @@ -203,8 +203,8 @@ - name: Copy icons ansible.builtin.copy: - src: usr/share/icons/{{ item }} - dest: /usr/share/icons/{{ item }} + src: "usr/local/share/icons/{{ item }}" + dest: "/usr/local/share/icons/{{ item }}" mode: '0444' loop: - gitea.png @@ -212,8 +212,8 @@ - name: Copy desktop files ansible.builtin.copy: - src: etc/skel/Desktop/{{ item }} - dest: /etc/skel/Desktop/{{ item }} + src: "etc/skel/Desktop/{{ item }}" + dest: "/etc/skel/Desktop/{{ item }}" mode: '0755' loop: - gitea.desktop @@ -224,7 +224,7 @@ - name: Set desktop file URLs ansible.builtin.lineinfile: - path: /etc/skel/Desktop/{{ item }}.desktop + path: "/etc/skel/Desktop/{{ item }}.desktop" line: "{{ lookup('file', '/root/{{ item }}.url') }}" state: present loop: diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop index 1c97fa3361..184609d263 100644 --- a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop @@ -3,4 +3,4 @@ Version=1.0 Type=Link Name=Gitea Comment= -Icon=/usr/share/icons/gitea.png +Icon=/usr/local/share/icons/gitea.png diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop index 52594cde0b..6a1c2b68c0 100644 --- a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop +++ b/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop @@ -3,4 +3,4 @@ Version=1.0 Type=Link Name=HedgeDoc Comment= -Icon=/usr/share/icons/hedgedoc.png +Icon=/usr/local/share/icons/hedgedoc.png diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png b/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/gitea.png deleted file mode 100644 index 7ca49a3918e4960dff2182c03c241ff23e997a8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8841 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 diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png b/data_safe_haven/resources/workspace/ansible/files/usr/share/icons/hedgedoc.png deleted file mode 100644 index 6dae00fd53caf67b3442cfba739c16b22cadddd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13773 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 Date: Thu, 8 Aug 2024 10:43:12 +0100 Subject: [PATCH 05/37] WIP: write vars from pulumi to desired state --- .../programs/declarative_sre.py | 4 ++-- .../infrastructure/programs/sre/data.py | 21 +++++++++++++++++++ .../infrastructure/programs/sre/workspaces.py | 6 ------ .../workspace/ansible/desired_state.yaml | 12 +++++------ .../etc/skel/Desktop/gitea.desktop.j2} | 1 + .../etc/skel/Desktop/hedgedoc.desktop.j2} | 1 + .../workspace.cloud_init.mustache.yaml | 10 --------- 7 files changed, 30 insertions(+), 25 deletions(-) rename data_safe_haven/resources/workspace/ansible/{files/etc/skel/Desktop/gitea.desktop => templates/etc/skel/Desktop/gitea.desktop.j2} (75%) rename data_safe_haven/resources/workspace/ansible/{files/etc/skel/Desktop/hedgedoc.desktop => templates/etc/skel/Desktop/hedgedoc.desktop.j2} (74%) diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index 81728d215c..32b671a740 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -178,6 +178,8 @@ def __call__(self) -> None: dns_private_zones=dns.private_zones, dns_record=networking.shm_ns_record, dns_server_admin_password=dns.password_admin, + gitea_hostname=user_services.gitea_server.hostname, + hedgedoc_hostname=user_services.hedgedoc_server.hostname, location=self.config.azure.location, resource_group=resource_group, sre_fqdn=networking.sre_fqdn, @@ -347,8 +349,6 @@ def __call__(self) -> None: data_collection_rule_id=monitoring.data_collection_rule_vms.id, data_collection_endpoint_id=monitoring.data_collection_endpoint.id, database_service_admin_password=data.password_database_service_admin, - gitea_hostname=user_services.gitea_server.hostname, - hedgedoc_hostname=user_services.hedgedoc_server.hostname, ldap_group_filter=ldap_group_filter, ldap_group_search_base=ldap_group_search_base, ldap_server_hostname=identity.hostname, diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index 524d743d44..f58c967f42 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -4,6 +4,7 @@ from typing import ClassVar import pulumi_random +import yaml from pulumi import ComponentResource, FileAsset, Input, Output, ResourceOptions from pulumi_azure_native import ( authorization, @@ -50,6 +51,8 @@ def __init__( dns_private_zones: Input[dict[str, network.PrivateZone]], dns_record: Input[network.RecordSet], dns_server_admin_password: Input[pulumi_random.RandomPassword], + gitea_hostname: Input[str], + hedgedoc_hostname: Input[str], location: Input[str], resource_group: Input[resources.ResourceGroup], sre_fqdn: Input[str], @@ -72,6 +75,8 @@ def __init__( ) self.dns_private_zones = dns_private_zones self.dns_record = dns_record + self.gitea_hostname = gitea_hostname + self.hedgedoc_hostname = hedgedoc_hostname self.password_dns_server_admin = dns_server_admin_password self.location = location self.resource_group_id = Output.from_input(resource_group).apply(get_id_from_rg) @@ -559,6 +564,18 @@ def __init__( resource_group_name=props.resource_group_name, source=file_asset, ) + # Upload ansible vars file + storage.Blob( + f"{container_desired_state._name}_blob_pulumi_vars", + account_name=storage_account_data_desired_state.name, + blob_name="vars/pulumi_vars.yaml", + container_name=container_desired_state.name, + resource_group_name=props.resource_group_name, + source=Output.all( + gitea_hostname=props.gitea_hostname, + hedgedoc_hostname=props.hedgedoc_hostname, + ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)), + ) # Set up a private endpoint for the desired state storage account storage_account_data_desired_state_endpoint = network.PrivateEndpoint( f"{storage_account_data_desired_state._name}_private_endpoint", @@ -940,3 +957,7 @@ def __init__( "key_vault_name": key_vault.name, "password_user_database_admin_secret": kvs_password_user_database_admin.name, } + + @staticmethod + def ansible_vars_file(**kwargs: str) -> str: + return yaml.safe_dump(kwargs, explicit_start=True, indent=2) diff --git a/data_safe_haven/infrastructure/programs/sre/workspaces.py b/data_safe_haven/infrastructure/programs/sre/workspaces.py index b48de97668..954bd06bb1 100644 --- a/data_safe_haven/infrastructure/programs/sre/workspaces.py +++ b/data_safe_haven/infrastructure/programs/sre/workspaces.py @@ -28,8 +28,6 @@ def __init__( data_collection_endpoint_id: Input[str], data_collection_rule_id: Input[str], database_service_admin_password: Input[str], - gitea_hostname: Input[str], - hedgedoc_hostname: Input[str], ldap_group_filter: Input[str], ldap_group_search_base: Input[str], ldap_server_hostname: Input[str], @@ -56,8 +54,6 @@ def __init__( self.data_collection_rule_id = data_collection_rule_id self.data_collection_endpoint_id = data_collection_endpoint_id self.database_service_admin_password = database_service_admin_password - self.gitea_hostname = gitea_hostname - self.hedgedoc_hostname = hedgedoc_hostname self.ldap_group_filter = ldap_group_filter self.ldap_group_search_base = ldap_group_search_base self.ldap_server_hostname = ldap_server_hostname @@ -120,8 +116,6 @@ def __init__( apt_proxy_server_hostname=props.apt_proxy_server_hostname, clamav_mirror_hostname=props.clamav_mirror_hostname, database_service_admin_password=props.database_service_admin_password, - gitea_hostname=props.gitea_hostname, - hedgedoc_hostname=props.hedgedoc_hostname, ldap_group_filter=props.ldap_group_filter, ldap_group_search_base=props.ldap_group_search_base, ldap_server_hostname=props.ldap_server_hostname, diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index c904a0bcf1..f5129c94e7 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -216,17 +216,15 @@ dest: "/etc/skel/Desktop/{{ item }}" mode: '0755' loop: - - gitea.desktop - - hedgedoc.desktop - input.desktop - output.desktop - shared.desktop - - name: Set desktop file URLs - ansible.builtin.lineinfile: - path: "/etc/skel/Desktop/{{ item }}.desktop" - line: "{{ lookup('file', '/root/{{ item }}.url') }}" - state: present + - name: Template Gitea and Hedgedoc desktop files + ansible.builtin.template: + src: "etc/skel/Desktop/{{ item }}.desktop.j2" + path: /etc/skel/Desktop/ + mode: '0755' loop: - gitea - hedgedoc diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop b/data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/gitea.desktop.j2 similarity index 75% rename from data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop rename to data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/gitea.desktop.j2 index 184609d263..082b955394 100644 --- a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/gitea.desktop +++ b/data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/gitea.desktop.j2 @@ -4,3 +4,4 @@ Type=Link Name=Gitea Comment= Icon=/usr/local/share/icons/gitea.png +URL=http://{{ gitea_hostname }} diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop b/data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/hedgedoc.desktop.j2 similarity index 74% rename from data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop rename to data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/hedgedoc.desktop.j2 index 6a1c2b68c0..23e10459ea 100644 --- a/data_safe_haven/resources/workspace/ansible/files/etc/skel/Desktop/hedgedoc.desktop +++ b/data_safe_haven/resources/workspace/ansible/templates/etc/skel/Desktop/hedgedoc.desktop.j2 @@ -4,3 +4,4 @@ Type=Link Name=HedgeDoc Comment= Icon=/usr/local/share/icons/hedgedoc.png +URL=http://{{ hedgedoc_hostname }} diff --git a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml index 408d39dc02..7137b94253 100644 --- a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml +++ b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml @@ -93,16 +93,6 @@ write_files: ansible-playbook /desired_state/desired_state.yaml popd - - path: "/root/gitea.url" - permissions: "0400" - content: | - URL=http://{{gitea_hostname}} - - - path: "/root/hedgedoc.url" - permissions: "0400" - content: | - URL=http://{{hedgedoc_hostname}} - mounts: # Desired state configuration is in a blob container mounted as NFSv3 - ["{{storage_account_data_desired_state_name}}.blob.core.windows.net:/{{storage_account_data_desired_state_name}}/desiredstate", /desired_state, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] From 1f6c0b964637bb974bc1a5349ea3affbaadc0e9a Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 11:16:38 +0100 Subject: [PATCH 06/37] Move desired state to new component --- .../programs/declarative_sre.py | 22 +- .../infrastructure/programs/sre/data.py | 174 +------------ .../programs/sre/desired_state.py | 236 ++++++++++++++++++ .../infrastructure/programs/sre/workspaces.py | 8 +- .../files/usr/local/share/icons/gitea.png | Bin 0 -> 8841 bytes .../files/usr/local/share/icons/hedgedoc.png | Bin 0 -> 13773 bytes 6 files changed, 258 insertions(+), 182 deletions(-) create mode 100644 data_safe_haven/infrastructure/programs/sre/desired_state.py create mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/gitea.png create mode 100644 data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/hedgedoc.png diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index 32b671a740..efdd7ae105 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -15,6 +15,7 @@ from .sre.backup import SREBackupComponent, SREBackupProps from .sre.clamav_mirror import SREClamAVMirrorComponent, SREClamAVMirrorProps from .sre.data import SREDataComponent, SREDataProps +from .sre.desired_state import SREDesiredStateComponent, SREDesiredStateProps from .sre.dns_server import SREDnsServerComponent, SREDnsServerProps from .sre.firewall import SREFirewallComponent, SREFirewallProps from .sre.identity import SREIdentityComponent, SREIdentityProps @@ -178,13 +179,10 @@ def __call__(self) -> None: dns_private_zones=dns.private_zones, dns_record=networking.shm_ns_record, dns_server_admin_password=dns.password_admin, - gitea_hostname=user_services.gitea_server.hostname, - hedgedoc_hostname=user_services.hedgedoc_server.hostname, location=self.config.azure.location, resource_group=resource_group, sre_fqdn=networking.sre_fqdn, subnet_data_configuration=networking.subnet_data_configuration, - subnet_data_desired_state=networking.subnet_data_desired_state, subnet_data_private=networking.subnet_data_private, subscription_id=self.config.azure.subscription_id, subscription_name=self.context.subscription_name, @@ -338,6 +336,22 @@ def __call__(self) -> None: tags=self.tags, ) + # Deploy desired state + desired_state = SREDesiredStateComponent( + "sre_desired_state", + self.stack_name, + SREDesiredStateProps( + admin_ip_addresses=self.config.sre.admin_ip_addresses, + dns_private_zones=dns.private_zones, + gitea_hostname=user_services.gitea_hostname, + hedgedoc_hostname=user_services.hedgedoc_hostname, + location=self.config.azure.location, + resource_group=resource_group, + subnet_desired_state=networking.subnet_desired_state, + subscription_name=self.context.subscription_name, + ), + ) + # Deploy workspaces workspaces = SREWorkspacesComponent( "sre_workspaces", @@ -360,7 +374,7 @@ def __call__(self) -> None: resource_group_name=resource_group.name, software_repository_hostname=user_services.software_repositories.hostname, sre_name=self.config.name, - storage_account_data_desired_state_name=data.storage_account_data_desired_state_name, + storage_account_desired_state_name=desired_state.storage_account_name, storage_account_data_private_user_name=data.storage_account_data_private_user_name, storage_account_data_private_sensitive_name=data.storage_account_data_private_sensitive_name, subnet_workspaces=networking.subnet_workspaces, diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index f58c967f42..92f911df56 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -5,7 +5,7 @@ import pulumi_random import yaml -from pulumi import ComponentResource, FileAsset, Input, Output, ResourceOptions +from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import ( authorization, keyvault, @@ -35,7 +35,6 @@ SSLCertificate, SSLCertificateProps, ) -from data_safe_haven.resources import resources_path from data_safe_haven.types import AzureDnsZoneNames @@ -51,13 +50,10 @@ def __init__( dns_private_zones: Input[dict[str, network.PrivateZone]], dns_record: Input[network.RecordSet], dns_server_admin_password: Input[pulumi_random.RandomPassword], - gitea_hostname: Input[str], - hedgedoc_hostname: Input[str], location: Input[str], resource_group: Input[resources.ResourceGroup], sre_fqdn: Input[str], subnet_data_configuration: Input[network.GetSubnetResult], - subnet_data_desired_state: Input[network.GetSubnetResult], subnet_data_private: Input[network.GetSubnetResult], subscription_id: Input[str], subscription_name: Input[str], @@ -75,8 +71,6 @@ def __init__( ) self.dns_private_zones = dns_private_zones self.dns_record = dns_record - self.gitea_hostname = gitea_hostname - self.hedgedoc_hostname = hedgedoc_hostname self.password_dns_server_admin = dns_server_admin_password self.location = location self.resource_group_id = Output.from_input(resource_group).apply(get_id_from_rg) @@ -87,9 +81,6 @@ def __init__( self.subnet_data_configuration_id = Output.from_input( subnet_data_configuration ).apply(get_id_from_subnet) - self.subnet_data_desired_state_id = Output.from_input( - subnet_data_desired_state - ).apply(get_id_from_subnet) self.subnet_data_private_id = Output.from_input(subnet_data_private).apply( get_id_from_subnet ) @@ -461,166 +452,6 @@ def __init__( ), ) - # Deploy desired state storage account - # - This holds the /desired_state container that is mounted by workspaces - # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer - storage_account_data_desired_state = storage.StorageAccount( - f"{self._name}_storage_account_data_desired_state", - # Storage account names have a maximum of 24 characters - account_name=alphanumeric( - f"{''.join(truncate_tokens(stack_name.split('-'), 11))}desiredstate{sha256hash(self._name)}" - )[:24], - enable_https_traffic_only=True, - enable_nfs_v3=True, - encryption=storage.EncryptionArgs( - key_source=storage.KeySource.MICROSOFT_STORAGE, - services=storage.EncryptionServicesArgs( - blob=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - file=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - ), - ), - kind=storage.Kind.BLOCK_BLOB_STORAGE, - is_hns_enabled=True, - location=props.location, - network_rule_set=storage.NetworkRuleSetArgs( - bypass=storage.Bypass.AZURE_SERVICES, - default_action=storage.DefaultAction.DENY, - ip_rules=Output.from_input(props.data_configuration_ip_addresses).apply( - lambda ip_ranges: [ - storage.IPRuleArgs( - action=storage.Action.ALLOW, - i_p_address_or_range=str(ip_address), - ) - for ip_range in sorted(ip_ranges) - for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() - ] - ), - virtual_network_rules=[ - storage.VirtualNetworkRuleArgs( - virtual_network_resource_id=props.subnet_data_desired_state_id, - ) - ], - ), - resource_group_name=props.resource_group_name, - sku=storage.SkuArgs(name=storage.SkuName.PREMIUM_ZRS), - opts=child_opts, - tags=child_tags, - ) - # Deploy desired state share - container_desired_state = storage.BlobContainer( - f"{self._name}_blob_desired_state", - account_name=storage_account_data_desired_state.name, - container_name="desiredstate", - default_encryption_scope="$account-encryption-key", - deny_encryption_scope_override=False, - public_access=storage.PublicAccess.NONE, - resource_group_name=props.resource_group_name, - opts=ResourceOptions.merge( - child_opts, - ResourceOptions(parent=storage_account_data_desired_state), - ), - ) - # Set storage container ACLs - BlobContainerAcl( - f"{container_desired_state._name}_acl", - BlobContainerAclProps( - acl_user="r-x", - acl_group="r-x", - acl_other="r-x", - # ensure that the above permissions are also set on any newly created - # files (eg. with Azure Storage Explorer) - apply_default_permissions=True, - container_name=container_desired_state.name, - resource_group_name=props.resource_group_name, - storage_account_name=storage_account_data_desired_state.name, - subscription_name=props.subscription_name, - ), - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=container_desired_state) - ), - ) - # Create file assets to upload - desired_state_directory = (resources_path / "workspace" / "ansible").absolute() - files_desired_state = [ - ( - FileAsset(str(file_path)), - file_path.name, - str(file_path.relative_to(desired_state_directory)), - ) - for file_path in sorted(desired_state_directory.rglob("*")) - if file_path.is_file() and not file_path.name.startswith(".") - ] - # Upload file assets to desired state container - for file_asset, file_name, file_path in files_desired_state: - storage.Blob( - f"{container_desired_state._name}_blob_{file_name}", - account_name=storage_account_data_desired_state.name, - blob_name=file_path, - container_name=container_desired_state.name, - resource_group_name=props.resource_group_name, - source=file_asset, - ) - # Upload ansible vars file - storage.Blob( - f"{container_desired_state._name}_blob_pulumi_vars", - account_name=storage_account_data_desired_state.name, - blob_name="vars/pulumi_vars.yaml", - container_name=container_desired_state.name, - resource_group_name=props.resource_group_name, - source=Output.all( - gitea_hostname=props.gitea_hostname, - hedgedoc_hostname=props.hedgedoc_hostname, - ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)), - ) - # Set up a private endpoint for the desired state storage account - storage_account_data_desired_state_endpoint = network.PrivateEndpoint( - f"{storage_account_data_desired_state._name}_private_endpoint", - location=props.location, - private_endpoint_name=f"{stack_name}-pep-storage-account-data-desired-state", - private_link_service_connections=[ - network.PrivateLinkServiceConnectionArgs( - group_ids=["blob"], - name=f"{stack_name}-cnxn-pep-storage-account-data-private-sensitive", - private_link_service_id=storage_account_data_desired_state.id, - ) - ], - resource_group_name=props.resource_group_name, - subnet=network.SubnetArgs(id=props.subnet_data_desired_state_id), - opts=ResourceOptions.merge( - child_opts, - ResourceOptions( - ignore_changes=["custom_dns_configs"], - parent=storage_account_data_desired_state, - ), - ), - tags=child_tags, - ) - # Add a private DNS record for each desired state endpoint custom DNS config - network.PrivateDnsZoneGroup( - f"{storage_account_data_desired_state._name}_private_dns_zone_group", - private_dns_zone_configs=[ - network.PrivateDnsZoneConfigArgs( - name=replace_separators( - f"{stack_name}-storage-account-data-desired-state-to-{dns_zone_name}", - "-", - ), - private_dns_zone_id=props.dns_private_zones[dns_zone_name].id, - ) - for dns_zone_name in AzureDnsZoneNames.STORAGE_ACCOUNT - ], - private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-desired-state", - private_endpoint_name=storage_account_data_desired_state_endpoint.name, - resource_group_name=props.resource_group_name, - opts=ResourceOptions.merge( - child_opts, - ResourceOptions(parent=storage_account_data_desired_state), - ), - ) - # Deploy sensitive data blob storage account # - This holds the /data and /output containers that are mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer @@ -928,9 +759,6 @@ def __init__( self.storage_account_data_configuration_name = ( storage_account_data_configuration.name ) - self.storage_account_data_desired_state_name = ( - storage_account_data_desired_state.name - ) self.managed_identity = identity_key_vault_reader self.password_nexus_admin = Output.secret(password_nexus_admin.result) self.password_database_service_admin = Output.secret( diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py new file mode 100644 index 0000000000..47710e6e02 --- /dev/null +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -0,0 +1,236 @@ +"""Pulumi component for SRE desired state""" + +from collections.abc import Mapping, Sequence + +from pulumi import ComponentResource, FileAsset, Input, Output, ResourceOptions +from pulumi_azure_native import ( + network, + resources, + storage, +) + +from data_safe_haven.external import AzureIPv4Range +from data_safe_haven.functions import ( + alphanumeric, + replace_separators, + sha256hash, + truncate_tokens, +) +from data_safe_haven.infrastructure.common import ( + get_id_from_rg, + get_id_from_subnet, + get_name_from_rg, +) +from data_safe_haven.infrastructure.components import ( + BlobContainerAcl, + BlobContainerAclProps, +) +from data_safe_haven.resources import resources_path +from data_safe_haven.types import AzureDnsZoneNames + + +class SREDesiredStateProps: + """Properties for SREDesiredStateComponent""" + + def __init__( + self, + admin_ip_addresses: Input[Sequence[str]], + dns_private_zones: Input[dict[str, network.PrivateZone]], + gitea_hostname: Input[str], + hedgedoc_hostname: Input[str], + location: Input[str], + resource_group: Input[resources.ResourceGroup], + subscription_name: Input[str], + subnet_desired_state: Input[network.GetSubnetResult], + ) -> None: + self.admin_ip_addresses = admin_ip_addresses + self.dns_private_zones = dns_private_zones + self.gitea_hostname = gitea_hostname + self.hedgedoc_hostname = hedgedoc_hostname + self.location = location + self.resource_group_id = Output.from_input(resource_group).apply(get_id_from_rg) + self.resource_group_name = Output.from_input(resource_group).apply( + get_name_from_rg + ) + self.subnet_desired_state_id = Output.from_input(subnet_desired_state).apply( + get_id_from_subnet + ) + self.subscription_name = subscription_name + + +class SREDesiredStateComponent(ComponentResource): + """Deploy SRE desired state with Pulumi""" + + def __init__( + self, + name: str, + stack_name: str, + props: SREDesiredStateProps, + opts: ResourceOptions | None = None, + tags: Input[Mapping[str, Input[str]]] | None = None, + ) -> None: + super().__init__("dsh:sre:DesiredStateComponent", name, {}, opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) + child_tags = {"component": "data"} | (tags if tags else {}) + + # Deploy desired state storage account + # - This holds the /desired_state container that is mounted by workspaces + # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer + storage_account = storage.StorageAccount( + f"{self._name}_storage_account", + # Storage account names have a maximum of 24 characters + account_name=alphanumeric( + f"{''.join(truncate_tokens(stack_name.split('-'), 11))}desiredstate{sha256hash(self._name)}" + )[:24], + enable_https_traffic_only=True, + enable_nfs_v3=True, + encryption=storage.EncryptionArgs( + key_source=storage.KeySource.MICROSOFT_STORAGE, + services=storage.EncryptionServicesArgs( + blob=storage.EncryptionServiceArgs( + enabled=True, key_type=storage.KeyType.ACCOUNT + ), + file=storage.EncryptionServiceArgs( + enabled=True, key_type=storage.KeyType.ACCOUNT + ), + ), + ), + kind=storage.Kind.BLOCK_BLOB_STORAGE, + is_hns_enabled=True, + location=props.location, + network_rule_set=storage.NetworkRuleSetArgs( + bypass=storage.Bypass.AZURE_SERVICES, + default_action=storage.DefaultAction.DENY, + ip_rules=Output.from_input(props.admin_ip_addresses).apply( + lambda ip_ranges: [ + storage.IPRuleArgs( + action=storage.Action.ALLOW, + i_p_address_or_range=str(ip_address), + ) + for ip_range in sorted(ip_ranges) + for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() + ] + ), + virtual_network_rules=[ + storage.VirtualNetworkRuleArgs( + virtual_network_resource_id=props.subnet_desired_state_id, + ) + ], + ), + resource_group_name=props.resource_group_name, + sku=storage.SkuArgs(name=storage.SkuName.PREMIUM_ZRS), + opts=child_opts, + tags=child_tags, + ) + # Deploy desired state share + container_desired_state = storage.BlobContainer( + f"{self._name}_blob_desired_state", + account_name=storage_account.name, + container_name="desiredstate", + default_encryption_scope="$account-encryption-key", + deny_encryption_scope_override=False, + public_access=storage.PublicAccess.NONE, + resource_group_name=props.resource_group_name, + opts=ResourceOptions.merge( + child_opts, + ResourceOptions(parent=storage_account), + ), + ) + # Set storage container ACLs + BlobContainerAcl( + f"{container_desired_state._name}_acl", + BlobContainerAclProps( + acl_user="r-x", + acl_group="r-x", + acl_other="r-x", + # ensure that the above permissions are also set on any newly created + # files (eg. with Azure Storage Explorer) + apply_default_permissions=True, + container_name=container_desired_state.name, + resource_group_name=props.resource_group_name, + storage_account_name=storage_account.name, + subscription_name=props.subscription_name, + ), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=container_desired_state) + ), + ) + # Create file assets to upload + desired_state_directory = (resources_path / "workspace" / "ansible").absolute() + files_desired_state = [ + ( + FileAsset(str(file_path)), + file_path.name, + str(file_path.relative_to(desired_state_directory)), + ) + for file_path in sorted(desired_state_directory.rglob("*")) + if file_path.is_file() and not file_path.name.startswith(".") + ] + # Upload file assets to desired state container + for file_asset, file_name, file_path in files_desired_state: + storage.Blob( + f"{container_desired_state._name}_blob_{file_name}", + account_name=storage_account.name, + blob_name=file_path, + container_name=container_desired_state.name, + resource_group_name=props.resource_group_name, + source=file_asset, + ) + # Upload ansible vars file + storage.Blob( + f"{container_desired_state._name}_blob_pulumi_vars", + account_name=storage_account.name, + blob_name="vars/pulumi_vars.yaml", + container_name=container_desired_state.name, + resource_group_name=props.resource_group_name, + source=Output.all( + gitea_hostname=props.gitea_hostname, + hedgedoc_hostname=props.hedgedoc_hostname, + ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)), + ) + # Set up a private endpoint for the desired state storage account + storage_account_endpoint = network.PrivateEndpoint( + f"{storage_account._name}_private_endpoint", + location=props.location, + private_endpoint_name=f"{stack_name}-pep-storage-account-data-desired-state", + private_link_service_connections=[ + network.PrivateLinkServiceConnectionArgs( + group_ids=["blob"], + name=f"{stack_name}-cnxn-pep-storage-account-data-private-sensitive", + private_link_service_id=storage_account.id, + ) + ], + resource_group_name=props.resource_group_name, + subnet=network.SubnetArgs(id=props.subnet_desired_state_id), + opts=ResourceOptions.merge( + child_opts, + ResourceOptions( + ignore_changes=["custom_dns_configs"], + parent=storage_account, + ), + ), + tags=child_tags, + ) + # Add a private DNS record for each desired state endpoint custom DNS config + network.PrivateDnsZoneGroup( + f"{storage_account._name}_private_dns_zone_group", + private_dns_zone_configs=[ + network.PrivateDnsZoneConfigArgs( + name=replace_separators( + f"{stack_name}-storage-account-data-desired-state-to-{dns_zone_name}", + "-", + ), + private_dns_zone_id=props.dns_private_zones[dns_zone_name].id, + ) + for dns_zone_name in AzureDnsZoneNames.STORAGE_ACCOUNT + ], + private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-desired-state", + private_endpoint_name=storage_account_endpoint.name, + resource_group_name=props.resource_group_name, + opts=ResourceOptions.merge( + child_opts, + ResourceOptions(parent=storage_account), + ), + ) + + self.storage_account_name = storage_account.name diff --git a/data_safe_haven/infrastructure/programs/sre/workspaces.py b/data_safe_haven/infrastructure/programs/sre/workspaces.py index 954bd06bb1..6d01767abb 100644 --- a/data_safe_haven/infrastructure/programs/sre/workspaces.py +++ b/data_safe_haven/infrastructure/programs/sre/workspaces.py @@ -39,7 +39,7 @@ def __init__( resource_group_name: Input[str], software_repository_hostname: Input[str], sre_name: Input[str], - storage_account_data_desired_state_name: Input[str], + storage_account_desired_state_name: Input[str], storage_account_data_private_sensitive_name: Input[str], storage_account_data_private_user_name: Input[str], subnet_workspaces: Input[network.GetSubnetResult], @@ -65,9 +65,7 @@ def __init__( self.resource_group_name = resource_group_name self.software_repository_hostname = software_repository_hostname self.sre_name = sre_name - self.storage_account_data_desired_state_name = ( - storage_account_data_desired_state_name - ) + self.storage_account_desired_state_name = storage_account_desired_state_name self.storage_account_data_private_user_name = ( storage_account_data_private_user_name ) @@ -123,7 +121,7 @@ def __init__( ldap_user_filter=props.ldap_user_filter, ldap_user_search_base=props.ldap_user_search_base, software_repository_hostname=props.software_repository_hostname, - storage_account_data_desired_state_name=props.storage_account_data_desired_state_name, + storage_account_desired_state_name=props.storage_account_desired_state_name, storage_account_data_private_user_name=props.storage_account_data_private_user_name, storage_account_data_private_sensitive_name=props.storage_account_data_private_sensitive_name, ).apply(lambda kwargs: self.template_cloudinit(**kwargs)) diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/gitea.png b/data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/gitea.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca49a3918e4960dff2182c03c241ff23e997a8d GIT binary patch literal 8841 zcmeHrcT`i`w{-%cNbev;LO_}XNT>;fE`op{Md=VifKZZ1LKg)EL5hMPy;?w#st5=O zC<>yY^bUd|QlvMfzJT}MpLe`B#{2FV-}~?8jFWxN+Vjl4*4%5aarU`xcIFf_13v=* z0AMyW&@-pBVFxEYE#<#i1O1NDGzD8)lg-fqV4^R<4d;yolY@v@Fg6h91^@&O`Iu>Q zG=u5uS7S7^!zDFZ75xh|r&nXIqz{nY?Gt*r!#9E&$2-Fl!>eT;-s9r3uW|0xyOPgx zBb<)a*l^QG=nU|w9)Fvhn5O33vSPWJ(r~H1Des1rUD| zyFnL$v)^s_&M61c0|3AXoUX2!p|0+qu}~t&3Qa^AG;8r)`&8f=YbiLS{YpgF@39g0 zg<>m7ok+72m9x&iyOH9|hB2{s$8CvA?iI7+pNk(|eFUc83sY57m8vzk{^@um5A4{; zXmiM?ZQo5HaHXfQ(l$p}k&V9iZ7$MJE7(#Dn3kE_;WtqiV z%~rE~4|tOVheR6Q`jx;dh0K%>LAk0grG3MHWCeERF>+}7I$Zb}PZ~sryX(h!yXC0r z$fSf=&!Er2EI;EuduaDcI=y*jKVJXZrbNm5Th2~o{++7F@BK0`255Gp1h9@awN;O^ zh<=@Z%yp)03DnNr8%~f^ol<4L?ybS5_T1Xxf$O01XB)5vY0l9ZUwPWdp(zB^;DpPpx!wojyLk8`mJk z8bJtup;S%CCS_>=Y-X*tXjN4~;%}3mFDx%O6F%*g%&aZag$?-@ce$_kQ*tgCN69T~ z6JsQX;3JE6CAeT^1AT~;qyqrdH3Nxgj2D&+cEP&i@F>V)T_XgHb45YUE1Aff5OuL0 zID=qctVQq{OH8mA2H^_P)L>8#L{bQRuw*ni(8n83LI$EBhrCG2^?_Lq0zQP0y-*Nq z6EmUea0%QXeWC_0R zaxeq}Atw))gTtW|1e6qnC!+(Qc#`M=#V-y$ED7U_Ba(3hJotbU?LzP)qaYAUKlo4o ze269{f6?Pff2cswLoN_al!M92%lY`o{r&`reA1sn@`pqJ;}axH$`X|`$C3zsz8LID ze=MFX`a8ubLld*V7!E>m$N3NsgQAG{x4~R7f8mIJzTSrzSBxCi8|yIzkaDY-$>s&Fh+UO`@71*WVluc)H*8D|#iYpZD=H>!bKwwo=U9o5v1vi)9C`^5El&nO1|L)ZRl`Dlx34=ka zs<^pAv8rw=P(?*o1t>z^RRxMrP=u){E27~pit>k42lIf`HZw#);IhA2%)HTLH-fJZ z3SxrBfK4s_gSEu@U@ge#1Jz(Ea5zE*rl_E(09S=6EBps^4(m&zWZVHMOkP$&^{~ek zgVd)`qA8h!^Fg~~<%oFq!;XV_K~kcjD2qNwR0{K6MQWR1aB1Nz*+DC zc=IyC)SQWn??zwUnBdgBh)5*U2ADv)T*uMkLRf2`}FpA@cNT^J8E-W^LR zAAjWQpLX2;kPC1nB^MVL1vnI=0HdT6T0sG-icpbWn1%$lf-`Pn7H*x^l z7pv_~(UGDRB|i_f0!#jyLaD#Q13a(?^FvV#Dz6It?~2L&5wYAs)%Y`Hb-Dj0iuxho zw=zR{?w5^Hz9`jD?yqwAhiC_d=l}5c$7K8;jzFRQcaVRi@4s^WE7w0#;2(ki&8~mt z`bP@tb|D53ql5-oD2{Sl_YKefJ4zxPuntZXg)hA(9$ZU>GGx(RwNw6 z1fo~Vpt_=^#l}s1LBqwy?ISS4@V$U|)JMoquW&eh=3EH*yT2e!47)|XU5K8RVq)l7 z5J;PbD#9|NJPImvWKg=TZngDU<{W-_?Q-N9y1#-nfi4TZk&nO7sBO9Ja;NNjB?PUMiWJ?k`!fc zzMq9^@#=u%Hh8Q`J=g6Cz;ZPop)gp}e3PFR#2I#>h{a%W0vJ;y^es$har32Z!+E(m zR3#}dgh^Y6p~HbCz<{B9GW~f(&%Nr3`lwgZpbB%2STIjC(3YyC{**oQr$_rG_cxw$rN{1UE(6rb=)~RS5yl&BoIiY*S^#@aZw-Fdblb1aKmN+rQWN7 z(mHN-JdXkV_EuAfMW2mQz70@gIL&c=W}{z*s@QR&IxwW+jc4vzt;$W+eN>BGK*iWc zzQs+Wo2!>^)*65>A!m(_5TCr881q=y{ZdA|$20=)(9V-d)zFPN|5kw7W_Pvl@-&j; zdJ}`$S*x>ROw3+AB1(W)0D`gq5PnSxurXN0C$`U`K3_8FbjwFfHjyE`Qy|(?M^a}^ z#Zlsg+j{=GL&AIN(z8>RoHiZpQ8YF8&jHiESFWcl(R5fS`3=C|*v3v38dw9x9wHgM zZawAaZxl_)=ilemE{p|@Ck{R@)~cP|m=oM+ma@K0>|_JqyU0=VGzV`1ET(IIlRxdWX_bzae0*r+a#ft5kcq@T6oNVjq3!FV>%C+kf&Q#dRq=q zvTJtre$%2oOLg7C84F91uY2;`VQibAhduSOPHN7N+medHH0fWc=Ek$u6igY!{UBMB z_u*L?!lz3d{q~evCc}@m@AD>HHq6YJ`{?{bbOHjLz=F8EHU^b}BZQTRn`}G1U*D`( zb8JMES+I3w^(Kf>=^ArsP^&Ly%=XP)xQm*&i$H&+$?6Kr=j%>sFbF`5{9G@teC{}` z&2FF75%$O=W;bfR@2H7j>l+4h5br6ayqoD)=qsKApSxKF@?AlvR`}~R+YhL`Q&K9t z(-2^oJDbw9P+ks@-2my1GN|XFWwpBbF72G@{X**yP~}AZ*@R2hjEc>FS}W&TB5^u4 zM!#mF)OizBY|C^6hQb{k1d7N!O#`4YbX>sJV)4so$-0Z;VyEtnt9HN!JYU{9N!rgl z&o{I!Zd(<7DizskDm=A~nzMOK5aK%m6=964oDvmhC^Sy*t%F_qUf^65PH!Yr&0NnbaxsKd(OFXNw{TPTMCbje z_3f#Z=0sCbU28y9E^nBwI74-R5LB;|vIK!m>Cfa4te>#gE+@H5s7S7S^_`qU;WdA+1vT*K&`kCWc*HGsUbAN*Wwg@JSze>>YS^UmJbW{{^VKT57HhmD>MkI;Pd zD5#Z6L44lCJZ2-FxO%&NVdJ`i{)RB2HvdMB#;fxo8h|{n9VPq0P$Kh&o)~pm&5fCW zkK==O``1e5p)X|UGv(GIsM4YA?#tP%e9Y>g`oTOqqdu^tSKNS3=+UuNcBFmZYxig7 zrm&z>B75yNb4?i916E(10s-D!WdPpSjHXGRl8Jb5C@Cqpi9A85xqvvLh6h zk|;FYk11~(zjZsglsahYgG$ha_Y(KSM=eNX*iu&Cy*EYp5ul86knj2NHd>m9IQy4! z8FT$k-9C`)p}zT!R1;Z2vZI-Dt3Xe|FM3fWf!nQAN?FO}u2%h**RIxd-_T}^} zCl`;-mz5i#Y06vNjz=4#MI6MHQ{&4w%r{vQf|`&?ywTKCL+-;abK4+}XHjnSj845j zHENHGZ_AutkSbZ|=uRY!jJQDpD1zE$5t-_NuubDrwS zZcT@o*oU?j+n2#LSDzu5a9U4=<2xTVDs!*Tja1R`l(-43lV={MIuHQ192klO6P zQorzxM#vnK6Y5{4yk1W2&z>j79^NwlkTvfJ$xeWqC)X`x&%Al_X;nc$on4K4(_+wW z16KV)_crep6i2qt&)6RpEg%g?wOG=9_uWfX528JSB^-q%f!13J`3)Xn2`hxAfJ=^R zviC+j0*3U1dDGKYX$A2@X~NM?qlLR3x01|_Q$Hma>r*I-BfM{c3^4V`8T!;oF9n#<-(2Eq;lb@W zQ>Tdj)U507Bg8yOwb4%NUmFW^HlEe>$Uq3!KUiaO`X0y{b}liyrl9{#?jSQy`|15- z=M{Dr(v5{+SARUf11QV+I&1%4wUfX0$=gv`U%aSA0G}X}6-b&UH7;+!*n?JPnWnO^ zMe<~!f1yv$j8M6eoZW7`l`al7?rZUd^!FrW>m8n992S`LPcC3Df+|>jslUV=9@*GM|fd3xd-% z&fsI}iSDIyskf|b80obh+i)-0nCzd$wAU2WqWlZzf#WCMLj@nc{%}`d>0{HACaJpa zDt1w+$q;AWTg~)SqCvOC2de@rk9$Z!tpt$Fe`_ykhzU+d!@LciJ97olN zZzFMbPRYr+qL^VOfUPwrU&?2vsi7B1n!SXJ##f}e34VRm6R1;iJ5d2_8`ij=GwF_Vyg@uIiOq=Sr&ZDVRvL`*O51 zAlYg2Evh~?=`{ji-~UB>sAb#Hd%gF@dq#`*Civa{{H6kk=nTsyJMb<{IkuF3qh)bG z`Zf?1l`(sb(df(*d@G#cblfOje?2Hm8QSjt!br;Id9(Z^h+pMi+$y5?;^QUB6Qdur ztXef^hmfDhGMeoJK=0a7+&jP+u#~}eUUhvILv7D5k)UJGxI_Dfv8v_f zd!E#*$O%3-jydT)}OVRwuTSo20dZE6D` z5XQV=qYSLnm+zQcWTgrQref;H?stBwsn~uc&BFWrE=efpGVE+gjoLawV@Q4X&WE=U z$Wav!-|UwbQ&Q-~PQiwDhtT{J!gH7p<2&U~2GrWP0B^$)ZVbve|2tk8b}lj+4)Z9= zoQmi4n=WM$Hg7?SlEc?g$tE$-9J zj7>?muAv{TXE}IL*{yRR$}0LHO>rCjE&ni+!~ke zCW%xDc%rHwtjWlQs^^e$cC;+>Uj>Q|@GC?;-%1~?uG1ZVHuDXD+D101tn5zC9_u_xQ7jqumca&7GR?nA4`uOD!GGh7=GhzL85zLrN$A zxzYf%Y;Bs4|D0nkNv-d8TKzEFR$oh$wj1$Uw%p>4TI{J(V^7K8J-Keef6j~b@r6z4 zn76ESMYE5OyCiB#SYJ5{s0wuiTeKdZ1hT4BW&2irXUp8gF74j@)CGrx`!5R_n;fl< zjuYwYN-S=EBs-ghY-L7wt(*4qQA^66aGjd{Cdq0%PB$J&j@sT%;01&Mzi_b78@vt_ z@w;m=+2;Ok&scfCCdUJs^QzBKgC;*DM4)ZHv>7s-&~b;ZU&7S&b97;J>_ddq)|`;h z;6-Qc@k*za$*T8MjlMEhgGK5_aYirRnB!Np%yr`8FHj%DM9&Ai+__kGS!oPx7qZTs z%Duc&es* z*h#SC;)`@ivwhj?YN@O9d!|%YR!)p-Gj$zoZngb#8spBQ;MM+w_v=wb5xT^YMx`ftl?}Y~>GL1vpZHub z=mYL%tRf)L3vsine4<-pXNG^U@D5U)V%Si?yfzY)9pQ*R)%pCI)76oHQ%xi>(C_YkGW8~H~t>N_w@bcp<&|U5gvoUnhF8UNg4onbVByF`5CIcsHKpKT##E>TJi!hRrxjZ z%a)ibwIrHosU{~Fhp}$9&Nw3cqGtPks^9(!D9<)4Pi0BAWt&-Nd2@zOty*KzA?MS* zi2K63Z}vC3B4ifj07e$FDJz4>saIFSD^rPK^3bC{jKt0Sr%`(JR$)zVcsFbi~I1 literal 0 HcmV?d00001 diff --git a/data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/hedgedoc.png b/data_safe_haven/resources/workspace/ansible/files/usr/local/share/icons/hedgedoc.png new file mode 100644 index 0000000000000000000000000000000000000000..6dae00fd53caf67b3442cfba739c16b22cadddd1 GIT binary patch literal 13773 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 Date: Thu, 8 Aug 2024 15:24:07 +0100 Subject: [PATCH 07/37] Correct hostname arguments --- data_safe_haven/infrastructure/programs/declarative_sre.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index efdd7ae105..5bd807db17 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -343,8 +343,8 @@ def __call__(self) -> None: SREDesiredStateProps( admin_ip_addresses=self.config.sre.admin_ip_addresses, dns_private_zones=dns.private_zones, - gitea_hostname=user_services.gitea_hostname, - hedgedoc_hostname=user_services.hedgedoc_hostname, + gitea_hostname=user_services.gitea_server.hostname, + hedgedoc_hostname=user_services.hedgedoc_server.hostname, location=self.config.azure.location, resource_group=resource_group, subnet_desired_state=networking.subnet_desired_state, From 384e9552b0da1310911edae92b61998ba3536514 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 15:32:00 +0100 Subject: [PATCH 08/37] Correct subnet variable names --- .../infrastructure/programs/sre/networking.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/networking.py b/data_safe_haven/infrastructure/programs/sre/networking.py index 5578d791a0..3faa4604c2 100644 --- a/data_safe_haven/infrastructure/programs/sre/networking.py +++ b/data_safe_haven/infrastructure/programs/sre/networking.py @@ -483,8 +483,8 @@ def __init__( opts=child_opts, tags=child_tags, ) - nsg_data_desired_state = network.NetworkSecurityGroup( - f"{self._name}_nsg_data_desired_state", + nsg_desired_state = network.NetworkSecurityGroup( + f"{self._name}_nsg_desired_state", location=props.location, network_security_group_name=f"{stack_name}-nsg-data-desired-state", resource_group_name=props.resource_group_name, @@ -1454,7 +1454,7 @@ def __init__( network.SecurityRuleArgs( access=network.SecurityRuleAccess.ALLOW, description="Allow outbound connections to desired state data endpoints.", - destination_address_prefix=SREIpRanges.data_desired_state.prefix, + destination_address_prefix=SREIpRanges.desired_state.prefix, destination_port_range="*", direction=network.SecurityRuleDirection.OUTBOUND, name="AllowDataDesiredStateEndpointsOutbound", @@ -1558,7 +1558,7 @@ def __init__( subnet_apt_proxy_server_name = "AptProxyServerSubnet" subnet_clamav_mirror_name = "ClamAVMirrorSubnet" subnet_data_configuration_name = "DataConfigurationSubnet" - subnet_data_desired_state_name = "DataDesiredStateSubnet" + subnet_desired_state_name = "DataDesiredStateSubnet" subnet_data_private_name = "DataPrivateSubnet" subnet_firewall_name = "AzureFirewallSubnet" subnet_firewall_management_name = "AzureFirewallManagementSubnet" @@ -1643,10 +1643,10 @@ def __init__( ), # Desired state data subnet network.SubnetArgs( - address_prefix=SREIpRanges.data_desired_state.prefix, - name=subnet_data_desired_state_name, + address_prefix=SREIpRanges.desired_state.prefix, + name=subnet_desired_state_name, network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_data_desired_state.id + id=nsg_desired_state.id ), route_table=network.RouteTableArgs(id=route_table.id), service_endpoints=[ @@ -1966,13 +1966,13 @@ def __init__( resource_group_name=props.resource_group_name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_data_desired_state = network.get_subnet_output( - subnet_name=subnet_data_desired_state_name, + self.subnet_desired_state = network.get_subnet_output( + subnet_name=subnet_desired_state_name, resource_group_name=props.resource_group_name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_data_desired_state = network.get_subnet_output( - subnet_name=subnet_data_desired_state_name, + self.subnet_desired_state = network.get_subnet_output( + subnet_name=subnet_desired_state_name, resource_group_name=props.resource_group_name, virtual_network_name=sre_virtual_network.name, ) From d5d07689163ed854bad7825759bb131d60e97432 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 15:39:17 +0100 Subject: [PATCH 09/37] Update NSG names --- .../infrastructure/programs/sre/desired_state.py | 8 ++++---- data_safe_haven/infrastructure/programs/sre/networking.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 47710e6e02..051c5f498a 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -192,11 +192,11 @@ def __init__( storage_account_endpoint = network.PrivateEndpoint( f"{storage_account._name}_private_endpoint", location=props.location, - private_endpoint_name=f"{stack_name}-pep-storage-account-data-desired-state", + private_endpoint_name=f"{stack_name}-pep-storage-account-desired-state", private_link_service_connections=[ network.PrivateLinkServiceConnectionArgs( group_ids=["blob"], - name=f"{stack_name}-cnxn-pep-storage-account-data-private-sensitive", + name=f"{stack_name}-cnxn-pep-storage-account-desired-state", private_link_service_id=storage_account.id, ) ], @@ -217,14 +217,14 @@ def __init__( private_dns_zone_configs=[ network.PrivateDnsZoneConfigArgs( name=replace_separators( - f"{stack_name}-storage-account-data-desired-state-to-{dns_zone_name}", + f"{stack_name}-storage-account-desired-state-to-{dns_zone_name}", "-", ), private_dns_zone_id=props.dns_private_zones[dns_zone_name].id, ) for dns_zone_name in AzureDnsZoneNames.STORAGE_ACCOUNT ], - private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-data-desired-state", + private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-desired-state", private_endpoint_name=storage_account_endpoint.name, resource_group_name=props.resource_group_name, opts=ResourceOptions.merge( diff --git a/data_safe_haven/infrastructure/programs/sre/networking.py b/data_safe_haven/infrastructure/programs/sre/networking.py index 3faa4604c2..c11d89bc17 100644 --- a/data_safe_haven/infrastructure/programs/sre/networking.py +++ b/data_safe_haven/infrastructure/programs/sre/networking.py @@ -486,7 +486,7 @@ def __init__( nsg_desired_state = network.NetworkSecurityGroup( f"{self._name}_nsg_desired_state", location=props.location, - network_security_group_name=f"{stack_name}-nsg-data-desired-state", + network_security_group_name=f"{stack_name}-nsg-desired-state", resource_group_name=props.resource_group_name, security_rules=[ # Inbound From 3766790d9d4ca41195bf7ebaec22de3c30cba56e Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Fri, 9 Aug 2024 15:06:14 +0100 Subject: [PATCH 10/37] Correct IP range name --- data_safe_haven/infrastructure/common/ip_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/common/ip_ranges.py b/data_safe_haven/infrastructure/common/ip_ranges.py index f0613e577a..2fb3279ca3 100644 --- a/data_safe_haven/infrastructure/common/ip_ranges.py +++ b/data_safe_haven/infrastructure/common/ip_ranges.py @@ -14,7 +14,7 @@ class SREIpRanges: apt_proxy_server = vnet.next_subnet(8) clamav_mirror = vnet.next_subnet(8) data_configuration = vnet.next_subnet(8) - data_desired_state = vnet.next_subnet(8) + desired_state = vnet.next_subnet(8) data_private = vnet.next_subnet(8) firewall = vnet.next_subnet(64) # 64 address minimum firewall_management = vnet.next_subnet(64) # 64 address minimum From dbccff71317ca270ed438c2f41564fe186b3e367 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Fri, 9 Aug 2024 15:13:43 +0100 Subject: [PATCH 11/37] Fix IP ranges test --- tests/infrastructure/common/test_ip_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/infrastructure/common/test_ip_ranges.py b/tests/infrastructure/common/test_ip_ranges.py index 3dab535193..20a60ff1bd 100644 --- a/tests/infrastructure/common/test_ip_ranges.py +++ b/tests/infrastructure/common/test_ip_ranges.py @@ -14,7 +14,7 @@ def test_vnet_and_subnets(self): assert SREIpRanges.data_configuration == AzureIPv4Range( "10.0.1.16", "10.0.1.23" ) - assert SREIpRanges.data_desired_state == AzureIPv4Range( + assert SREIpRanges.desired_state == AzureIPv4Range( "10.0.1.24", "10.0.1.31" ) assert SREIpRanges.data_private == AzureIPv4Range("10.0.1.32", "10.0.1.39") From 7a41029ab002fbabefa5754cf6b38bcf1be42d73 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Fri, 9 Aug 2024 15:34:07 +0100 Subject: [PATCH 12/37] Move ansible vars file function --- data_safe_haven/infrastructure/programs/sre/data.py | 5 ----- data_safe_haven/infrastructure/programs/sre/desired_state.py | 5 +++++ tests/infrastructure/common/test_ip_ranges.py | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index 92f911df56..e9f653effd 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -4,7 +4,6 @@ from typing import ClassVar import pulumi_random -import yaml from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import ( authorization, @@ -785,7 +784,3 @@ def __init__( "key_vault_name": key_vault.name, "password_user_database_admin_secret": kvs_password_user_database_admin.name, } - - @staticmethod - def ansible_vars_file(**kwargs: str) -> str: - return yaml.safe_dump(kwargs, explicit_start=True, indent=2) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 051c5f498a..7faec9bc20 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -2,6 +2,7 @@ from collections.abc import Mapping, Sequence +import yaml from pulumi import ComponentResource, FileAsset, Input, Output, ResourceOptions from pulumi_azure_native import ( network, @@ -234,3 +235,7 @@ def __init__( ) self.storage_account_name = storage_account.name + + @staticmethod + def ansible_vars_file(**kwargs: str) -> str: + return yaml.safe_dump(kwargs, explicit_start=True, indent=2) diff --git a/tests/infrastructure/common/test_ip_ranges.py b/tests/infrastructure/common/test_ip_ranges.py index 20a60ff1bd..807c4f3f86 100644 --- a/tests/infrastructure/common/test_ip_ranges.py +++ b/tests/infrastructure/common/test_ip_ranges.py @@ -14,9 +14,7 @@ def test_vnet_and_subnets(self): assert SREIpRanges.data_configuration == AzureIPv4Range( "10.0.1.16", "10.0.1.23" ) - assert SREIpRanges.desired_state == AzureIPv4Range( - "10.0.1.24", "10.0.1.31" - ) + assert SREIpRanges.desired_state == AzureIPv4Range("10.0.1.24", "10.0.1.31") assert SREIpRanges.data_private == AzureIPv4Range("10.0.1.32", "10.0.1.39") assert SREIpRanges.firewall == AzureIPv4Range("10.0.1.64", "10.0.1.127") assert SREIpRanges.firewall_management == AzureIPv4Range( From 5c193fea9ee82773d3fe2525ab2b3a2223c8a6d8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 09:40:03 +0100 Subject: [PATCH 13/37] Use string asset for blob --- .../programs/sre/desired_state.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 7faec9bc20..52d90db5e5 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -3,7 +3,14 @@ from collections.abc import Mapping, Sequence import yaml -from pulumi import ComponentResource, FileAsset, Input, Output, ResourceOptions +from pulumi import ( + ComponentResource, + FileAsset, + Input, + Output, + ResourceOptions, + StringAsset, +) from pulumi_azure_native import ( network, resources, @@ -184,10 +191,12 @@ def __init__( blob_name="vars/pulumi_vars.yaml", container_name=container_desired_state.name, resource_group_name=props.resource_group_name, - source=Output.all( - gitea_hostname=props.gitea_hostname, - hedgedoc_hostname=props.hedgedoc_hostname, - ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)), + source=StringAsset( + Output.all( + gitea_hostname=props.gitea_hostname, + hedgedoc_hostname=props.hedgedoc_hostname, + ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)) + ), ) # Set up a private endpoint for the desired state storage account storage_account_endpoint = network.PrivateEndpoint( From d5920c8b8cf36e2ef6ac104b161f438fad8ad793 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 09:48:41 +0100 Subject: [PATCH 14/37] Apply StringAsset to str not Output[str] --- .../infrastructure/programs/sre/desired_state.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 52d90db5e5..e84345fc4d 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -191,11 +191,11 @@ def __init__( blob_name="vars/pulumi_vars.yaml", container_name=container_desired_state.name, resource_group_name=props.resource_group_name, - source=StringAsset( - Output.all( - gitea_hostname=props.gitea_hostname, - hedgedoc_hostname=props.hedgedoc_hostname, - ).apply(lambda kwargs: self.ansible_vars_file(**kwargs)) + source=Output.all( + gitea_hostname=props.gitea_hostname, + hedgedoc_hostname=props.hedgedoc_hostname, + ).apply( + lambda kwargs: StringAsset(self.ansible_vars_file(**kwargs)) ), ) # Set up a private endpoint for the desired state storage account From b0febc764d6c1552e8a8f13b5cd97b6033d27004 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 09:57:24 +0100 Subject: [PATCH 15/37] Add StringAsset to typings --- data_safe_haven/infrastructure/programs/sre/desired_state.py | 4 +--- typings/pulumi/__init__.pyi | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index e84345fc4d..8700939af0 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -194,9 +194,7 @@ def __init__( source=Output.all( gitea_hostname=props.gitea_hostname, hedgedoc_hostname=props.hedgedoc_hostname, - ).apply( - lambda kwargs: StringAsset(self.ansible_vars_file(**kwargs)) - ), + ).apply(lambda kwargs: StringAsset(self.ansible_vars_file(**kwargs))), ) # Set up a private endpoint for the desired state storage account storage_account_endpoint = network.PrivateEndpoint( diff --git a/typings/pulumi/__init__.pyi b/typings/pulumi/__init__.pyi index e1468220dd..80ae2bf986 100644 --- a/typings/pulumi/__init__.pyi +++ b/typings/pulumi/__init__.pyi @@ -1,6 +1,6 @@ import pulumi.automation as automation import pulumi.dynamic as dynamic -from pulumi.asset import FileAsset +from pulumi.asset import FileAsset, StringAsset from pulumi.config import ( Config, ) @@ -27,5 +27,6 @@ __all__ = [ "Output", "Resource", "ResourceOptions", + "StringAsset", "UNKNOWN", ] From 9b48edb8996b4489c7306aecdd3d3f0da9b73b5c Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 10:08:54 +0100 Subject: [PATCH 16/37] Correct cloudinit template argument name --- .../resources/workspace/workspace.cloud_init.mustache.yaml | 2 +- tests/infrastructure/programs/sre/test_workspaces.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml index 7137b94253..cfa8aebfd4 100644 --- a/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml +++ b/data_safe_haven/resources/workspace/workspace.cloud_init.mustache.yaml @@ -95,7 +95,7 @@ write_files: mounts: # Desired state configuration is in a blob container mounted as NFSv3 - - ["{{storage_account_data_desired_state_name}}.blob.core.windows.net:/{{storage_account_data_desired_state_name}}/desiredstate", /desired_state, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] + - ["{{storage_account_desired_state_name}}.blob.core.windows.net:/{{storage_account_desired_state_name}}/desiredstate", /desired_state, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] # Secure data is in a blob container mounted as NFSv3 - ["{{storage_account_data_private_sensitive_name}}.blob.core.windows.net:/{{storage_account_data_private_sensitive_name}}/ingress", /data, nfs, "ro,_netdev,sec=sys,vers=3,nolock,proto=tcp"] - ["{{storage_account_data_private_sensitive_name}}.blob.core.windows.net:/{{storage_account_data_private_sensitive_name}}/egress", /output, nfs, "rw,_netdev,sec=sys,vers=3,nolock,proto=tcp"] diff --git a/tests/infrastructure/programs/sre/test_workspaces.py b/tests/infrastructure/programs/sre/test_workspaces.py index 108ce162d0..e4a395eeef 100644 --- a/tests/infrastructure/programs/sre/test_workspaces.py +++ b/tests/infrastructure/programs/sre/test_workspaces.py @@ -6,10 +6,10 @@ class TestTemplateCloudInit: def test_template_cloudinit(self): cloudinit = SREWorkspacesComponent.template_cloudinit( - storage_account_data_desired_state_name="storageaccount", + storage_account_desired_state_name="sadesiredstate", ) assert ( - '- ["storageaccount.blob.core.windows.net:/storageaccount/desiredstate", /desired_state, nfs, "ro,' + '- ["sadesiredstate.blob.core.windows.net:/sadesiredstate/desiredstate", /desired_state, nfs, "ro,' in cloudinit ) From 56d4e8a532d975e520525f15f84099d74b017dff Mon Sep 17 00:00:00 2001 From: James Robinson Date: Mon, 12 Aug 2024 14:09:22 +0100 Subject: [PATCH 17/37] :bug: Ensure that icons directory is created --- .../workspace/ansible/desired_state.yaml | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index f5129c94e7..c87ca873ce 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -54,6 +54,18 @@ - apt-news - esm-cache + - name: Ensure necessary directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /etc/skel/Desktop + - /etc/xdg/xfce4/terminal + - /etc/xrdp + - /usr/local/share/icons + - /usr/local/share/xrdp + - name: Enable bash autocompletion globally ansible.builtin.blockinfile: path: /etc/bash.bashrc @@ -124,17 +136,6 @@ validate: sshd -T -f %s notify: Restart sshd - - name: Ensure xrdp directories exist - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: '0755' - loop: - - /etc/skel/Desktop - - /etc/xdg/xfce4/terminal - - /etc/xrdp - - /usr/local/share/xrdp - - name: Copy xrdp settings ansible.builtin.copy: src: etc/xrdp/xrdp.ini From 913ccd0037083fb1af816a868c3a6d43c49a07b6 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Mon, 12 Aug 2024 16:08:47 +0100 Subject: [PATCH 18/37] :recycle: Simplify ansible setup by copying all files in a directory where possible --- .../workspace/ansible/desired_state.yaml | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index c87ca873ce..b26afeeb33 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -172,7 +172,7 @@ regexp: '^TerminalEmulator=' line: 'TerminalEmulator=xfce4-terminal' - - name: Set default terminal colourscheme + - name: Copy default terminal colourscheme ansible.builtin.copy: src: etc/xdg/xfce4/terminal/terminalrc dest: /etc/xdg/xfce4/terminal/terminalrc @@ -202,24 +202,19 @@ - xrdp - xrdp-sesman - - name: Copy icons + # This will create directories if src or dest ends in '/' + - name: Copy desktop icons directory ansible.builtin.copy: - src: "usr/local/share/icons/{{ item }}" - dest: "/usr/local/share/icons/{{ item }}" + src: "usr/local/share/icons/" + dest: "/usr/local/share/icons/" mode: '0444' - loop: - - gitea.png - - hedgedoc.png - - name: Copy desktop files + # This will create directories if src or dest ends in '/' + - name: Copy desktop files directory ansible.builtin.copy: - src: "etc/skel/Desktop/{{ item }}" - dest: "/etc/skel/Desktop/{{ item }}" + src: "etc/skel/Desktop/" + dest: "/etc/skel/Desktop/" mode: '0755' - loop: - - input.desktop - - output.desktop - - shared.desktop - name: Template Gitea and Hedgedoc desktop files ansible.builtin.template: @@ -293,12 +288,12 @@ systemctl stop clamav-freshclam && freshclam && systemctl start clamav-freshclam creates: '/var/lib/clamav/main.{c[vl]d,inc}' - - name: Install ClamAV services and timer + # This will create directories if src or dest ends in '/' + - name: Copy ClamAV services directory ansible.builtin.copy: - src: "{{ item }}" + src: "etc/systemd/system/" dest: /etc/systemd/system/ mode: '0644' - with_fileglob: 'etc/systemd/system/clamav*' notify: Systemd daemon reload - name: Enable and start freshclam @@ -325,12 +320,12 @@ enabled: true state: started - - name: Copy smoke test files + # This will create directories if src or dest ends in '/' + - name: Copy smoke test files directory ansible.builtin.copy: - src: "{{ item }}" + src: "usr/local/smoke_tests/" dest: /usr/local/smoke_tests/ mode: '0755' - with_fileglob: 'usr/local/smoke_tests/*' handlers: @@ -344,10 +339,6 @@ name: sshd state: restarted - - name: Update PAM auth # noqa: no-changed-when - ansible.builtin.command: - cmd: pam-auth-update --enable mkhomedir ldap - # Run systemd daemon-reload. # https://www.freedesktop.org/software/systemd/man/systemctl.html#daemon-reload # Should be called when changes are made to .service or .timer files From 7eb831710afb8836dec708916dfa2eb1a8f41bd5 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Mon, 12 Aug 2024 16:12:35 +0100 Subject: [PATCH 19/37] :wrench: Do not create xrdp directories separately --- .../workspace/ansible/desired_state.yaml | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index b26afeeb33..2dcee486f9 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -54,18 +54,6 @@ - apt-news - esm-cache - - name: Ensure necessary directories exist - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: '0755' - loop: - - /etc/skel/Desktop - - /etc/xdg/xfce4/terminal - - /etc/xrdp - - /usr/local/share/icons - - /usr/local/share/xrdp - - name: Enable bash autocompletion globally ansible.builtin.blockinfile: path: /etc/bash.bashrc @@ -136,16 +124,18 @@ validate: sshd -T -f %s notify: Restart sshd + # This will create directories if src or dest ends in '/' - name: Copy xrdp settings ansible.builtin.copy: - src: etc/xrdp/xrdp.ini - dest: /etc/xrdp/xrdp.ini + src: etc/xrdp/ + dest: /etc/xrdp/ mode: '0644' + # This will create directories if src or dest ends in '/' - name: Copy xrdp logo ansible.builtin.copy: - src: usr/local/share/xrdp/dsh_logo_240x140_256color.bmp - dest: /usr/local/share/xrdp/dsh_logo_240x140_256color.bmp + src: usr/local/share/xrdp/ + dest: /usr/local/share/xrdp/ mode: '0444' - name: Disable xrdp root login @@ -172,10 +162,11 @@ regexp: '^TerminalEmulator=' line: 'TerminalEmulator=xfce4-terminal' + # This will create directories if src or dest ends in '/' - name: Copy default terminal colourscheme ansible.builtin.copy: - src: etc/xdg/xfce4/terminal/terminalrc - dest: /etc/xdg/xfce4/terminal/terminalrc + src: etc/xdg/xfce4/terminal/ + dest: /etc/xdg/xfce4/terminal/ mode: '0444' - name: Use a blank screensaver From d5fc6b7189cbb4031a8bd713b46dd76054641222 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 11:36:21 +0100 Subject: [PATCH 20/37] Tidy ansible --- .../workspace/ansible/desired_state.yaml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 2dcee486f9..9dbd411a24 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -124,14 +124,12 @@ validate: sshd -T -f %s notify: Restart sshd - # This will create directories if src or dest ends in '/' - name: Copy xrdp settings ansible.builtin.copy: src: etc/xrdp/ dest: /etc/xrdp/ mode: '0644' - # This will create directories if src or dest ends in '/' - name: Copy xrdp logo ansible.builtin.copy: src: usr/local/share/xrdp/ @@ -162,7 +160,6 @@ regexp: '^TerminalEmulator=' line: 'TerminalEmulator=xfce4-terminal' - # This will create directories if src or dest ends in '/' - name: Copy default terminal colourscheme ansible.builtin.copy: src: etc/xdg/xfce4/terminal/ @@ -193,18 +190,16 @@ - xrdp - xrdp-sesman - # This will create directories if src or dest ends in '/' - name: Copy desktop icons directory ansible.builtin.copy: - src: "usr/local/share/icons/" - dest: "/usr/local/share/icons/" + src: usr/local/share/icons/ + dest: /usr/local/share/icons/ mode: '0444' - # This will create directories if src or dest ends in '/' - name: Copy desktop files directory ansible.builtin.copy: - src: "etc/skel/Desktop/" - dest: "/etc/skel/Desktop/" + src: etc/skel/Desktop/ + dest: /etc/skel/Desktop/ mode: '0755' - name: Template Gitea and Hedgedoc desktop files @@ -279,10 +274,9 @@ systemctl stop clamav-freshclam && freshclam && systemctl start clamav-freshclam creates: '/var/lib/clamav/main.{c[vl]d,inc}' - # This will create directories if src or dest ends in '/' - name: Copy ClamAV services directory ansible.builtin.copy: - src: "etc/systemd/system/" + src: etc/systemd/system/ dest: /etc/systemd/system/ mode: '0644' notify: Systemd daemon reload @@ -311,10 +305,9 @@ enabled: true state: started - # This will create directories if src or dest ends in '/' - name: Copy smoke test files directory ansible.builtin.copy: - src: "usr/local/smoke_tests/" + src: usr/local/smoke_tests/ dest: /usr/local/smoke_tests/ mode: '0755' From 57b8d144b06df6dd100752873bcad318f390f6c5 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 13:55:08 +0100 Subject: [PATCH 21/37] Remove unused notify --- data_safe_haven/resources/workspace/ansible/desired_state.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 9dbd411a24..b9a65f7d0f 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -96,7 +96,6 @@ new_module_path: pam_mkhomedir.so module_arguments: 'skel=/etc/skel umask=0022' state: after - notify: Update PAM auth - name: Don't prompt to change expired passwords via ldap community.general.pamd: From 3e1eb306532c57f3953152e16c9d81405fec803c Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 13:59:54 +0100 Subject: [PATCH 22/37] Correct template argument --- data_safe_haven/resources/workspace/ansible/desired_state.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index b9a65f7d0f..88706cd01d 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -204,7 +204,7 @@ - name: Template Gitea and Hedgedoc desktop files ansible.builtin.template: src: "etc/skel/Desktop/{{ item }}.desktop.j2" - path: /etc/skel/Desktop/ + dest: /etc/skel/Desktop/ mode: '0755' loop: - gitea From c67fe5b857401118e1b3e26ac1d2bc2b73e8c0c5 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 14:03:37 +0100 Subject: [PATCH 23/37] Use vars file in desired state play --- data_safe_haven/resources/workspace/ansible/desired_state.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 88706cd01d..c34e3939f3 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -2,6 +2,8 @@ - name: Desired state configuration hosts: localhost become: true + vars_files: + - vars/pulumi_vars.yaml tasks: - name: Update package cache From 9427541b55716fc2e072f6d96a0bd42d3e551dc8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 14:09:23 +0100 Subject: [PATCH 24/37] Correct desktop file name --- data_safe_haven/resources/workspace/ansible/desired_state.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index c34e3939f3..5ec58cf64c 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -206,7 +206,7 @@ - name: Template Gitea and Hedgedoc desktop files ansible.builtin.template: src: "etc/skel/Desktop/{{ item }}.desktop.j2" - dest: /etc/skel/Desktop/ + dest: "/etc/skel/Desktop/{{ item }}.desktop" mode: '0755' loop: - gitea From 546764eb2bbc3eba4923b82faaf1614ab5c70825 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 11:20:07 +0100 Subject: [PATCH 25/37] Wait for clamd.ctl to be created --- .../resources/workspace/ansible/desired_state.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 5ec58cf64c..93a0f3f6f9 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -294,6 +294,10 @@ enabled: true state: started + - name: Wait for ClamAV daemon to finish initialising + ansible.builtin.wait_for: + path: /var/run/clamav/clamd.ctl + - name: Enable and start ClamAV on access scan ansible.builtin.systemd: name: clamav-clamonacc From 3fcc1aebed5ddf80e8abc6189aacdfdc122f1723 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 14:48:44 +0100 Subject: [PATCH 26/37] Restart clamd when configuration is updated --- .../workspace/ansible/desired_state.yaml | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 93a0f3f6f9..2e1a4e742c 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -254,13 +254,26 @@ creates: /etc/audit/rules.d/50-privileged.rules notify: Restart auditd - - name: Copy ClamAV configuration + - name: Copy ClamAV daemon configuration ansible.builtin.copy: src: etc/clamav/clamd.conf dest: /etc/clamav/clamd.conf mode: '0444' owner: clamav group: adm + register: clamd + + - name: Enable and start ClamAV daemon + ansible.builtin.systemd: + name: clamav-daemon + enabled: true + state: started + + - name: Restart ClamAV daemon # noqa: no-handler + ansible.builtin.systemd: + name: clamav-daemon + state: restarted + when: clamd.changed - name: Set freshclam private mirror ansible.builtin.lineinfile: @@ -288,16 +301,6 @@ state: started enabled: true - - name: Enable and start ClamAV daemon - ansible.builtin.systemd: - name: clamav-daemon - enabled: true - state: started - - - name: Wait for ClamAV daemon to finish initialising - ansible.builtin.wait_for: - path: /var/run/clamav/clamd.ctl - - name: Enable and start ClamAV on access scan ansible.builtin.systemd: name: clamav-clamonacc From 3edb2145c1048cb3a8d9e641a48acc085abd0771 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 8 Aug 2024 15:34:41 +0100 Subject: [PATCH 27/37] Correct socket file name --- .../ansible/files/etc/systemd/system/clamav-clamonacc.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/resources/workspace/ansible/files/etc/systemd/system/clamav-clamonacc.service b/data_safe_haven/resources/workspace/ansible/files/etc/systemd/system/clamav-clamonacc.service index 0adf095db8..6320ed19b7 100644 --- a/data_safe_haven/resources/workspace/ansible/files/etc/systemd/system/clamav-clamonacc.service +++ b/data_safe_haven/resources/workspace/ansible/files/etc/systemd/system/clamav-clamonacc.service @@ -6,7 +6,7 @@ After=clamav-daemon.service syslog.target network.target [Service] Type=simple User=root -ExecStartPre=/bin/bash -c "while [ ! -S /var/run/clamav/clamd.ctl ]; do sleep 1; done" +ExecStartPre=/bin/bash -c "while [ ! -S /tmp/clamd.socket ]; do sleep 1; done" ExecStart=/usr/sbin/clamonacc --foreground=true Restart=on-failure RestartSec=30 From 626be8017c950ce2ebbd01aa107215450f63410e Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 13 Aug 2024 14:32:49 +0100 Subject: [PATCH 28/37] Move cloud init template to ansible --- .../workspace/ansible/desired_state.yaml | 24 +++++++- .../ansible/templates/etc/R/Rprofile.site | 6 ++ .../templates/etc/database_credential.j2 | 1 + .../ansible/templates/etc/nslcd.conf.j2 | 24 ++++++++ .../ansible/templates/etc/pip.conf.j2 | 5 ++ .../workspace.cloud_init.mustache.yaml | 57 ------------------- 6 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 data_safe_haven/resources/workspace/ansible/templates/etc/R/Rprofile.site create mode 100644 data_safe_haven/resources/workspace/ansible/templates/etc/database_credential.j2 create mode 100644 data_safe_haven/resources/workspace/ansible/templates/etc/nslcd.conf.j2 create mode 100644 data_safe_haven/resources/workspace/ansible/templates/etc/pip.conf.j2 diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 2e1a4e742c..98c2444f17 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -87,6 +87,12 @@ regexp: '^(passwd|group|shadow)(:.*)(? Date: Mon, 2 Sep 2024 10:27:42 +0100 Subject: [PATCH 29/37] Update data_safe_haven/infrastructure/common/ip_ranges.py Co-authored-by: James Robinson --- data_safe_haven/infrastructure/common/ip_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/infrastructure/common/ip_ranges.py b/data_safe_haven/infrastructure/common/ip_ranges.py index 2fb3279ca3..aa201f3878 100644 --- a/data_safe_haven/infrastructure/common/ip_ranges.py +++ b/data_safe_haven/infrastructure/common/ip_ranges.py @@ -14,8 +14,8 @@ class SREIpRanges: apt_proxy_server = vnet.next_subnet(8) clamav_mirror = vnet.next_subnet(8) data_configuration = vnet.next_subnet(8) - desired_state = vnet.next_subnet(8) data_private = vnet.next_subnet(8) + desired_state = vnet.next_subnet(8) firewall = vnet.next_subnet(64) # 64 address minimum firewall_management = vnet.next_subnet(64) # 64 address minimum guacamole_containers = vnet.next_subnet(8) From 96fb5518c6e9016808a69ee60bc2f62c427f4a6f Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Mon, 2 Sep 2024 16:25:43 +0100 Subject: [PATCH 30/37] Correct ip range test --- tests/infrastructure/common/test_ip_ranges.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/infrastructure/common/test_ip_ranges.py b/tests/infrastructure/common/test_ip_ranges.py index 807c4f3f86..2c4dc749d3 100644 --- a/tests/infrastructure/common/test_ip_ranges.py +++ b/tests/infrastructure/common/test_ip_ranges.py @@ -14,8 +14,8 @@ def test_vnet_and_subnets(self): assert SREIpRanges.data_configuration == AzureIPv4Range( "10.0.1.16", "10.0.1.23" ) - assert SREIpRanges.desired_state == AzureIPv4Range("10.0.1.24", "10.0.1.31") - assert SREIpRanges.data_private == AzureIPv4Range("10.0.1.32", "10.0.1.39") + assert SREIpRanges.data_private == AzureIPv4Range("10.0.1.24", "10.0.1.31") + assert SREIpRanges.desired_state == AzureIPv4Range("10.0.1.32", "10.0.1.39") assert SREIpRanges.firewall == AzureIPv4Range("10.0.1.64", "10.0.1.127") assert SREIpRanges.firewall_management == AzureIPv4Range( "10.0.1.128", "10.0.1.191" From 22c754f4c0b50f9d91df2faf2f75333088ced505 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 17 Sep 2024 10:28:12 +0100 Subject: [PATCH 31/37] Add NFSV3StorageAccount component --- .../infrastructure/components/__init__.py | 2 + .../components/wrapped/__init__.py | 2 + .../wrapped/nfsv3_storage_account.py | 68 +++++++++++++++++++ .../infrastructure/programs/sre/data.py | 43 ++---------- .../programs/sre/desired_state.py | 42 ++---------- 5 files changed, 80 insertions(+), 77 deletions(-) create mode 100644 data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py diff --git a/data_safe_haven/infrastructure/components/__init__.py b/data_safe_haven/infrastructure/components/__init__.py index cc6bcb15a4..5273fade9b 100644 --- a/data_safe_haven/infrastructure/components/__init__.py +++ b/data_safe_haven/infrastructure/components/__init__.py @@ -19,6 +19,7 @@ SSLCertificateProps, ) from .wrapped import ( + NFSV3StorageAccount, WrappedLogAnalyticsWorkspace, ) @@ -34,6 +35,7 @@ "LocalDnsRecordProps", "MicrosoftSQLDatabaseComponent", "MicrosoftSQLDatabaseProps", + "NFSV3StorageAccount", "PostgresqlDatabaseComponent", "PostgresqlDatabaseProps", "SSLCertificate", diff --git a/data_safe_haven/infrastructure/components/wrapped/__init__.py b/data_safe_haven/infrastructure/components/wrapped/__init__.py index fc5f8c8f61..48c5570168 100644 --- a/data_safe_haven/infrastructure/components/wrapped/__init__.py +++ b/data_safe_haven/infrastructure/components/wrapped/__init__.py @@ -1,5 +1,7 @@ from .log_analytics_workspace import WrappedLogAnalyticsWorkspace +from .nfsv3_storage_account import NFSV3StorageAccount __all__ = [ + "NFSV3StorageAccount", "WrappedLogAnalyticsWorkspace", ] diff --git a/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py b/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py new file mode 100644 index 0000000000..0787970fef --- /dev/null +++ b/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py @@ -0,0 +1,68 @@ +from collections.abc import Mapping, Sequence + +from pulumi import Input, Output, ResourceOptions +from pulumi_azure_native import storage + +from data_safe_haven.external import AzureIPv4Range + + +class NFSV3StorageAccount(storage.StorageAccount): + encryption_args = storage.EncryptionArgs( + key_source=storage.KeySource.MICROSOFT_STORAGE, + services=storage.EncryptionServicesArgs( + blob=storage.EncryptionServiceArgs( + enabled=True, key_type=storage.KeyType.ACCOUNT + ), + file=storage.EncryptionServiceArgs( + enabled=True, key_type=storage.KeyType.ACCOUNT + ), + ), + ) + + def __init__( + self, + resource_name: str, + *, + account_name: Input[str], + allowed_ip_addresses: Input[Sequence[str]], + location: Input[str], + resource_group_name: Input[str], + subnet_id: Input[str], + opts: ResourceOptions, + tags: Input[Mapping[str, Input[str]]], + ): + self.resource_group_name_ = Output.from_input(resource_group_name) + super().__init__( + resource_name, + account_name=account_name, + enable_https_traffic_only=True, + enable_nfs_v3=True, + encryption=self.encryption_args, + is_hns_enabled=True, + kind=storage.Kind.BLOCK_BLOB_STORAGE, + location=location, + minimum_tls_version=storage.MinimumTlsVersion.TLS1_2, + network_rule_set=storage.NetworkRuleSetArgs( + bypass=storage.Bypass.AZURE_SERVICES, + default_action=storage.DefaultAction.DENY, + ip_rules=Output.from_input(allowed_ip_addresses).apply( + lambda ip_ranges: [ + storage.IPRuleArgs( + action=storage.Action.ALLOW, + i_p_address_or_range=str(ip_address), + ) + for ip_range in sorted(ip_ranges) + for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() + ] + ), + virtual_network_rules=[ + storage.VirtualNetworkRuleArgs( + virtual_network_resource_id=subnet_id, + ) + ], + ), + resource_group_name=resource_group_name, + sku=storage.SkuArgs(name=storage.SkuName.PREMIUM_ZRS), + opts=opts, + tags=tags, + ) diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index cff4045db3..6cf5edb869 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -31,6 +31,7 @@ from data_safe_haven.infrastructure.components import ( BlobContainerAcl, BlobContainerAclProps, + NFSV3StorageAccount, SSLCertificate, SSLCertificateProps, ) @@ -459,52 +460,16 @@ def __init__( # Deploy sensitive data blob storage account # - This holds the /data and /output containers that are mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer - storage_account_data_private_sensitive = storage.StorageAccount( + storage_account_data_private_sensitive = NFSV3StorageAccount( f"{self._name}_storage_account_data_private_sensitive", # Storage account names have a maximum of 24 characters account_name=alphanumeric( f"{''.join(truncate_tokens(stack_name.split('-'), 11))}sensitivedata{sha256hash(self._name)}" )[:24], - enable_https_traffic_only=True, - enable_nfs_v3=True, - encryption=storage.EncryptionArgs( - key_source=storage.KeySource.MICROSOFT_STORAGE, - services=storage.EncryptionServicesArgs( - blob=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - file=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - ), - ), - kind=storage.Kind.BLOCK_BLOB_STORAGE, - is_hns_enabled=True, + allowed_ip_addresses=props.data_private_sensitive_ip_addresses, location=props.location, - minimum_tls_version=storage.MinimumTlsVersion.TLS1_2, - network_rule_set=storage.NetworkRuleSetArgs( - bypass=storage.Bypass.AZURE_SERVICES, - default_action=storage.DefaultAction.DENY, - ip_rules=Output.from_input( - props.data_private_sensitive_ip_addresses - ).apply( - lambda ip_ranges: [ - storage.IPRuleArgs( - action=storage.Action.ALLOW, - i_p_address_or_range=str(ip_address), - ) - for ip_range in sorted(ip_ranges) - for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() - ] - ), - virtual_network_rules=[ - storage.VirtualNetworkRuleArgs( - virtual_network_resource_id=props.subnet_data_private_id, - ) - ], - ), + subnet_id=props.subnet_data_private_id, resource_group_name=props.resource_group_name, - sku=storage.SkuArgs(name=storage.SkuName.PREMIUM_ZRS), opts=child_opts, tags=child_tags, ) diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 8700939af0..8e1fc6c8a0 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -17,7 +17,6 @@ storage, ) -from data_safe_haven.external import AzureIPv4Range from data_safe_haven.functions import ( alphanumeric, replace_separators, @@ -32,6 +31,7 @@ from data_safe_haven.infrastructure.components import ( BlobContainerAcl, BlobContainerAclProps, + NFSV3StorageAccount, ) from data_safe_haven.resources import resources_path from data_safe_haven.types import AzureDnsZoneNames @@ -84,49 +84,15 @@ def __init__( # Deploy desired state storage account # - This holds the /desired_state container that is mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer - storage_account = storage.StorageAccount( + storage_account = NFSV3StorageAccount( f"{self._name}_storage_account", - # Storage account names have a maximum of 24 characters account_name=alphanumeric( f"{''.join(truncate_tokens(stack_name.split('-'), 11))}desiredstate{sha256hash(self._name)}" )[:24], - enable_https_traffic_only=True, - enable_nfs_v3=True, - encryption=storage.EncryptionArgs( - key_source=storage.KeySource.MICROSOFT_STORAGE, - services=storage.EncryptionServicesArgs( - blob=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - file=storage.EncryptionServiceArgs( - enabled=True, key_type=storage.KeyType.ACCOUNT - ), - ), - ), - kind=storage.Kind.BLOCK_BLOB_STORAGE, - is_hns_enabled=True, + allowed_ip_addresses=props.admin_ip_addresses, location=props.location, - network_rule_set=storage.NetworkRuleSetArgs( - bypass=storage.Bypass.AZURE_SERVICES, - default_action=storage.DefaultAction.DENY, - ip_rules=Output.from_input(props.admin_ip_addresses).apply( - lambda ip_ranges: [ - storage.IPRuleArgs( - action=storage.Action.ALLOW, - i_p_address_or_range=str(ip_address), - ) - for ip_range in sorted(ip_ranges) - for ip_address in AzureIPv4Range.from_cidr(ip_range).all_ips() - ] - ), - virtual_network_rules=[ - storage.VirtualNetworkRuleArgs( - virtual_network_resource_id=props.subnet_desired_state_id, - ) - ], - ), resource_group_name=props.resource_group_name, - sku=storage.SkuArgs(name=storage.SkuName.PREMIUM_ZRS), + subnet_id=props.subnet_desired_state_id, opts=child_opts, tags=child_tags, ) From 435697b1effa8fd6c4a71d52cced829ce30aa75a Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 17 Sep 2024 14:18:56 +0100 Subject: [PATCH 32/37] Add composite resource for NFS blob containers --- .../infrastructure/components/__init__.py | 4 + .../components/composite/__init__.py | 3 + .../composite/nfsv3_blob_container.py | 75 +++++++++++++++++++ .../infrastructure/programs/sre/data.py | 55 +++----------- .../programs/sre/desired_state.py | 29 ++----- 5 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 data_safe_haven/infrastructure/components/composite/nfsv3_blob_container.py diff --git a/data_safe_haven/infrastructure/components/__init__.py b/data_safe_haven/infrastructure/components/__init__.py index 5273fade9b..89a127b8d8 100644 --- a/data_safe_haven/infrastructure/components/__init__.py +++ b/data_safe_haven/infrastructure/components/__init__.py @@ -4,6 +4,8 @@ LocalDnsRecordProps, MicrosoftSQLDatabaseComponent, MicrosoftSQLDatabaseProps, + NFSV3BlobContainerComponent, + NFSV3BlobContainerProps, PostgresqlDatabaseComponent, PostgresqlDatabaseProps, VMComponent, @@ -35,6 +37,8 @@ "LocalDnsRecordProps", "MicrosoftSQLDatabaseComponent", "MicrosoftSQLDatabaseProps", + "NFSV3BlobContainerComponent", + "NFSV3BlobContainerProps", "NFSV3StorageAccount", "PostgresqlDatabaseComponent", "PostgresqlDatabaseProps", diff --git a/data_safe_haven/infrastructure/components/composite/__init__.py b/data_safe_haven/infrastructure/components/composite/__init__.py index f111bab028..e4254a50ed 100644 --- a/data_safe_haven/infrastructure/components/composite/__init__.py +++ b/data_safe_haven/infrastructure/components/composite/__init__.py @@ -3,6 +3,7 @@ MicrosoftSQLDatabaseComponent, MicrosoftSQLDatabaseProps, ) +from .nfsv3_blob_container import NFSV3BlobContainerComponent, NFSV3BlobContainerProps from .postgresql_database import PostgresqlDatabaseComponent, PostgresqlDatabaseProps from .virtual_machine import LinuxVMComponentProps, VMComponent @@ -12,6 +13,8 @@ "LocalDnsRecordProps", "MicrosoftSQLDatabaseComponent", "MicrosoftSQLDatabaseProps", + "NFSV3BlobContainerComponent", + "NFSV3BlobContainerProps", "PostgresqlDatabaseComponent", "PostgresqlDatabaseProps", "VMComponent", diff --git a/data_safe_haven/infrastructure/components/composite/nfsv3_blob_container.py b/data_safe_haven/infrastructure/components/composite/nfsv3_blob_container.py new file mode 100644 index 0000000000..98564918a0 --- /dev/null +++ b/data_safe_haven/infrastructure/components/composite/nfsv3_blob_container.py @@ -0,0 +1,75 @@ +from pulumi import ComponentResource, Input, ResourceOptions +from pulumi_azure_native import storage + +from data_safe_haven.infrastructure.components.dynamic.blob_container_acl import ( + BlobContainerAcl, + BlobContainerAclProps, +) + + +class NFSV3BlobContainerProps: + def __init__( + self, + acl_user: Input[str], + acl_group: Input[str], + acl_other: Input[str], + apply_default_permissions: Input[bool], + container_name: Input[str], + resource_group_name: Input[str], + storage_account: Input[storage.StorageAccount], + subscription_name: Input[str], + ): + self.acl_user = acl_user + self.acl_group = acl_group + self.acl_other = acl_other + self.apply_default_permissions = apply_default_permissions + self.container_name = container_name + self.resource_group_name = resource_group_name + self.storage_account = storage_account + self.subscription_name = subscription_name + + +class NFSV3BlobContainerComponent(ComponentResource): + def __init__( + self, + name: str, + props: NFSV3BlobContainerProps, + opts: ResourceOptions | None = None, + ): + super().__init__("dsh:common:NFSV3BlobContainerComponent", name, {}, opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) + + storage_container = storage.BlobContainer( + f"{self._name}_blob_container_{props.container_name}", + account_name=props.storage_account.name, + container_name=props.container_name, + default_encryption_scope="$account-encryption-key", + deny_encryption_scope_override=False, + public_access=storage.PublicAccess.NONE, + resource_group_name=props.resource_group_name, + opts=ResourceOptions.merge( + child_opts, + ResourceOptions(parent=props.storage_account), + ), + ) + BlobContainerAcl( + f"{storage_container._name}_acl", + BlobContainerAclProps( + acl_user=props.acl_user, + acl_group=props.acl_group, + acl_other=props.acl_other, + apply_default_permissions=props.apply_default_permissions, + container_name=storage_container.name, + resource_group_name=props.resource_group_name, + storage_account_name=props.storage_account.name, + subscription_name=props.subscription_name, + ), + opts=ResourceOptions.merge( + child_opts, + ResourceOptions(parent=props.storage_account), + ), + ) + + self.name = storage_container.name + + self.register_outputs({}) diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index 6cf5edb869..e416eaa8e8 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -29,8 +29,8 @@ get_name_from_rg, ) from data_safe_haven.infrastructure.components import ( - BlobContainerAcl, - BlobContainerAclProps, + NFSV3BlobContainerComponent, + NFSV3BlobContainerProps, NFSV3StorageAccount, SSLCertificate, SSLCertificateProps, @@ -474,68 +474,35 @@ def __init__( tags=child_tags, ) # Deploy storage containers - storage_container_egress = storage.BlobContainer( + NFSV3BlobContainerComponent( f"{self._name}_blob_egress", - account_name=storage_account_data_private_sensitive.name, - container_name="egress", - default_encryption_scope="$account-encryption-key", - deny_encryption_scope_override=False, - public_access=storage.PublicAccess.NONE, - resource_group_name=props.resource_group_name, - opts=ResourceOptions.merge( - child_opts, - ResourceOptions(parent=storage_account_data_private_sensitive), - ), - ) - storage_container_ingress = storage.BlobContainer( - f"{self._name}_blob_ingress", - account_name=storage_account_data_private_sensitive.name, - container_name="ingress", - default_encryption_scope="$account-encryption-key", - deny_encryption_scope_override=False, - public_access=storage.PublicAccess.NONE, - resource_group_name=props.resource_group_name, - opts=ResourceOptions.merge( - child_opts, - ResourceOptions(parent=storage_account_data_private_sensitive), - ), - ) - # Set storage container ACLs - BlobContainerAcl( - f"{storage_container_egress._name}_acl", - BlobContainerAclProps( + NFSV3BlobContainerProps( acl_user="rwx", acl_group="rwx", acl_other="rwx", # due to an Azure bug `apply_default_permissions=True` also gives user # 65533 ownership of the fileshare (preventing use inside the SRE) apply_default_permissions=False, - container_name=storage_container_egress.name, + container_name="egress", resource_group_name=props.resource_group_name, - storage_account_name=storage_account_data_private_sensitive.name, + storage_account=storage_account_data_private_sensitive, subscription_name=props.subscription_name, ), - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_container_egress) - ), ) - BlobContainerAcl( - f"{storage_container_ingress._name}_acl", - BlobContainerAclProps( + NFSV3BlobContainerComponent( + f"{self._name}_blob_ingress", + NFSV3BlobContainerProps( acl_user="rwx", acl_group="r-x", acl_other="r-x", # ensure that the above permissions are also set on any newly created # files (eg. with Azure Storage Explorer) apply_default_permissions=True, - container_name=storage_container_ingress.name, + container_name="ingress", resource_group_name=props.resource_group_name, - storage_account_name=storage_account_data_private_sensitive.name, + storage_account=storage_account_data_private_sensitive, subscription_name=props.subscription_name, ), - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=storage_container_ingress) - ), ) # Set up a private endpoint for the sensitive data storage account storage_account_data_private_sensitive_endpoint = network.PrivateEndpoint( diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 8e1fc6c8a0..5ec650d7e9 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -29,8 +29,8 @@ get_name_from_rg, ) from data_safe_haven.infrastructure.components import ( - BlobContainerAcl, - BlobContainerAclProps, + NFSV3BlobContainerComponent, + NFSV3BlobContainerProps, NFSV3StorageAccount, ) from data_safe_haven.resources import resources_path @@ -97,37 +97,20 @@ def __init__( tags=child_tags, ) # Deploy desired state share - container_desired_state = storage.BlobContainer( + container_desired_state = NFSV3BlobContainerComponent( f"{self._name}_blob_desired_state", - account_name=storage_account.name, - container_name="desiredstate", - default_encryption_scope="$account-encryption-key", - deny_encryption_scope_override=False, - public_access=storage.PublicAccess.NONE, - resource_group_name=props.resource_group_name, - opts=ResourceOptions.merge( - child_opts, - ResourceOptions(parent=storage_account), - ), - ) - # Set storage container ACLs - BlobContainerAcl( - f"{container_desired_state._name}_acl", - BlobContainerAclProps( + NFSV3BlobContainerProps( acl_user="r-x", acl_group="r-x", acl_other="r-x", # ensure that the above permissions are also set on any newly created # files (eg. with Azure Storage Explorer) apply_default_permissions=True, - container_name=container_desired_state.name, + container_name="desiredstate", resource_group_name=props.resource_group_name, - storage_account_name=storage_account.name, + storage_account=storage_account, subscription_name=props.subscription_name, ), - opts=ResourceOptions.merge( - child_opts, ResourceOptions(parent=container_desired_state) - ), ) # Create file assets to upload desired_state_directory = (resources_path / "workspace" / "ansible").absolute() From 2bae206d125ad4db721d47dc3023bc4f802e51f7 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 17 Sep 2024 15:09:44 +0100 Subject: [PATCH 33/37] Correct path to install_deb script --- data_safe_haven/resources/workspace/ansible/desired_state.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 7287d3d262..3361ffe760 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -29,7 +29,7 @@ tags: apt ansible.builtin.script: executable: /bin/bash - cmd: "/desired_state/install_deb.sh {{ item.source }} {{ item.filename }} {{ item.sha256 }}" + cmd: "/var/local/ansible/install_deb.sh {{ item.source }} {{ item.filename }} {{ item.sha256 }}" creates: "{{ item.creates }}" loop: "{{ deb_packages[ansible_facts.distribution_release] }}" From cb8969792ec9fb4bbb3e2be3323c7ce64bb62616 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 17 Sep 2024 15:11:46 +0100 Subject: [PATCH 34/37] Correct paths in comments --- data_safe_haven/infrastructure/programs/sre/data.py | 4 ++-- data_safe_haven/infrastructure/programs/sre/desired_state.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index e416eaa8e8..ede49ba62c 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -458,7 +458,7 @@ def __init__( ), ) # Deploy sensitive data blob storage account - # - This holds the /data and /output containers that are mounted by workspaces + # - This holds the /mnt/input and /mnt/output containers that are mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer storage_account_data_private_sensitive = NFSV3StorageAccount( f"{self._name}_storage_account_data_private_sensitive", @@ -569,7 +569,7 @@ def __init__( ) # Deploy data_private_user files storage account - # - This holds the /home and /shared containers that are mounted by workspaces + # - This holds the /home and /mnt/shared containers that are mounted by workspaces # - Azure Files has better NFS support but cannot be accessed with Azure Storage Explorer # - Allows root-squashing to be configured # From https://learn.microsoft.com/en-us/azure/storage/files/files-nfs-protocol diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 5ec650d7e9..185fbfabfc 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -82,7 +82,7 @@ def __init__( child_tags = {"component": "data"} | (tags if tags else {}) # Deploy desired state storage account - # - This holds the /desired_state container that is mounted by workspaces + # - This holds the /var/local/ansible container that is mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer storage_account = NFSV3StorageAccount( f"{self._name}_storage_account", From aa46d236dc62a78414a384a9486b84c91a2e74e8 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 17 Sep 2024 15:42:49 +0100 Subject: [PATCH 35/37] Update pulumi component properties --- .../programs/declarative_sre.py | 20 +++++------ .../programs/sre/desired_state.py | 27 +++++++++++++++ .../infrastructure/programs/sre/workspaces.py | 33 ------------------- 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/data_safe_haven/infrastructure/programs/declarative_sre.py b/data_safe_haven/infrastructure/programs/declarative_sre.py index 0017474f6a..483d21355b 100644 --- a/data_safe_haven/infrastructure/programs/declarative_sre.py +++ b/data_safe_haven/infrastructure/programs/declarative_sre.py @@ -344,11 +344,20 @@ def __call__(self) -> None: self.stack_name, SREDesiredStateProps( admin_ip_addresses=self.config.sre.admin_ip_addresses, + clamav_mirror_hostname=clamav_mirror.hostname, + database_service_admin_password=data.password_database_service_admin, dns_private_zones=dns.private_zones, gitea_hostname=user_services.gitea_server.hostname, hedgedoc_hostname=user_services.hedgedoc_server.hostname, + ldap_group_filter=ldap_group_filter, + ldap_group_search_base=ldap_group_search_base, + ldap_server_hostname=identity.hostname, + ldap_server_port=identity.server_port, + ldap_user_filter=ldap_user_filter, + ldap_user_search_base=ldap_user_search_base, location=self.config.azure.location, resource_group=resource_group, + software_repository_hostname=user_services.software_repositories.hostname, subnet_desired_state=networking.subnet_desired_state, subscription_name=self.context.subscription_name, ), @@ -361,22 +370,11 @@ def __call__(self) -> None: SREWorkspacesProps( admin_password=data.password_workspace_admin, apt_proxy_server_hostname=apt_proxy_server.hostname, - clamav_mirror_hostname=clamav_mirror.hostname, data_collection_rule_id=monitoring.data_collection_rule_vms.id, data_collection_endpoint_id=monitoring.data_collection_endpoint.id, - database_service_admin_password=data.password_database_service_admin, - gitea_hostname=user_services.gitea_server.hostname, - hedgedoc_hostname=user_services.hedgedoc_server.hostname, - ldap_group_filter=ldap_group_filter, - ldap_group_search_base=ldap_group_search_base, - ldap_server_hostname=identity.hostname, - ldap_server_port=identity.server_port, - ldap_user_filter=ldap_user_filter, - ldap_user_search_base=ldap_user_search_base, location=self.config.azure.location, maintenance_configuration_id=monitoring.maintenance_configuration.id, resource_group_name=resource_group.name, - software_repository_hostname=user_services.software_repositories.hostname, sre_name=self.config.name, storage_account_desired_state_name=desired_state.storage_account_name, storage_account_data_private_user_name=data.storage_account_data_private_user_name, diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 185fbfabfc..94ccf2b737 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -43,23 +43,41 @@ class SREDesiredStateProps: def __init__( self, admin_ip_addresses: Input[Sequence[str]], + clamav_mirror_hostname: Input[str], + database_service_admin_password: Input[str], dns_private_zones: Input[dict[str, network.PrivateZone]], gitea_hostname: Input[str], hedgedoc_hostname: Input[str], + ldap_group_filter: Input[str], + ldap_group_search_base: Input[str], + ldap_server_hostname: Input[str], + ldap_server_port: Input[int], + ldap_user_filter: Input[str], + ldap_user_search_base: Input[str], location: Input[str], resource_group: Input[resources.ResourceGroup], + software_repository_hostname: Input[str], subscription_name: Input[str], subnet_desired_state: Input[network.GetSubnetResult], ) -> None: self.admin_ip_addresses = admin_ip_addresses + self.clamav_mirror_hostname = clamav_mirror_hostname + self.database_service_admin_password = database_service_admin_password self.dns_private_zones = dns_private_zones self.gitea_hostname = gitea_hostname self.hedgedoc_hostname = hedgedoc_hostname + self.ldap_group_filter = ldap_group_filter + self.ldap_group_search_base = ldap_group_search_base + self.ldap_server_hostname = ldap_server_hostname + self.ldap_server_port = Output.from_input(ldap_server_port).apply(str) + self.ldap_user_filter = ldap_user_filter + self.ldap_user_search_base = ldap_user_search_base self.location = location self.resource_group_id = Output.from_input(resource_group).apply(get_id_from_rg) self.resource_group_name = Output.from_input(resource_group).apply( get_name_from_rg ) + self.software_repository_hostname = software_repository_hostname self.subnet_desired_state_id = Output.from_input(subnet_desired_state).apply( get_id_from_subnet ) @@ -141,8 +159,17 @@ def __init__( container_name=container_desired_state.name, resource_group_name=props.resource_group_name, source=Output.all( + clamav_mirror_hostname=props.clamav_mirror_hostname, + database_service_admin_password=props.database_service_admin_password, gitea_hostname=props.gitea_hostname, hedgedoc_hostname=props.hedgedoc_hostname, + ldap_group_filter=props.ldap_group_filter, + ldap_group_search_base=props.ldap_group_search_base, + ldap_server_hostname=props.ldap_server_hostname, + ldap_server_port=props.ldap_server_port, + ldap_user_filter=props.ldap_user_filter, + ldap_user_search_base=props.ldap_user_search_base, + software_repository_hostname=props.software_repository_hostname, ).apply(lambda kwargs: StringAsset(self.ansible_vars_file(**kwargs))), ) # Set up a private endpoint for the desired state storage account diff --git a/data_safe_haven/infrastructure/programs/sre/workspaces.py b/data_safe_haven/infrastructure/programs/sre/workspaces.py index ca8a605963..d6c22bce53 100644 --- a/data_safe_haven/infrastructure/programs/sre/workspaces.py +++ b/data_safe_haven/infrastructure/programs/sre/workspaces.py @@ -24,22 +24,11 @@ def __init__( self, admin_password: Input[str], apt_proxy_server_hostname: Input[str], - clamav_mirror_hostname: Input[str], data_collection_endpoint_id: Input[str], data_collection_rule_id: Input[str], - database_service_admin_password: Input[str], - gitea_hostname: Input[str], - hedgedoc_hostname: Input[str], - ldap_group_filter: Input[str], - ldap_group_search_base: Input[str], - ldap_server_hostname: Input[str], - ldap_server_port: Input[int], - ldap_user_filter: Input[str], - ldap_user_search_base: Input[str], location: Input[str], maintenance_configuration_id: Input[str], resource_group_name: Input[str], - software_repository_hostname: Input[str], sre_name: Input[str], storage_account_desired_state_name: Input[str], storage_account_data_private_sensitive_name: Input[str], @@ -52,22 +41,11 @@ def __init__( self.admin_password = Output.secret(admin_password) self.admin_username = "dshadmin" self.apt_proxy_server_hostname = apt_proxy_server_hostname - self.clamav_mirror_hostname = clamav_mirror_hostname self.data_collection_rule_id = data_collection_rule_id self.data_collection_endpoint_id = data_collection_endpoint_id - self.database_service_admin_password = database_service_admin_password - self.gitea_hostname = gitea_hostname - self.hedgedoc_hostname = hedgedoc_hostname - self.ldap_group_filter = ldap_group_filter - self.ldap_group_search_base = ldap_group_search_base - self.ldap_server_hostname = ldap_server_hostname - self.ldap_server_port = Output.from_input(ldap_server_port).apply(str) - self.ldap_user_filter = ldap_user_filter - self.ldap_user_search_base = ldap_user_search_base self.location = location self.maintenance_configuration_id = maintenance_configuration_id self.resource_group_name = resource_group_name - self.software_repository_hostname = software_repository_hostname self.sre_name = sre_name self.storage_account_desired_state_name = storage_account_desired_state_name self.storage_account_data_private_user_name = ( @@ -116,17 +94,6 @@ def __init__( # Load cloud-init file cloudinit = Output.all( apt_proxy_server_hostname=props.apt_proxy_server_hostname, - clamav_mirror_hostname=props.clamav_mirror_hostname, - database_service_admin_password=props.database_service_admin_password, - gitea_hostname=props.gitea_hostname, - hedgedoc_hostname=props.hedgedoc_hostname, - ldap_group_filter=props.ldap_group_filter, - ldap_group_search_base=props.ldap_group_search_base, - ldap_server_hostname=props.ldap_server_hostname, - ldap_server_port=props.ldap_server_port, - ldap_user_filter=props.ldap_user_filter, - ldap_user_search_base=props.ldap_user_search_base, - software_repository_hostname=props.software_repository_hostname, storage_account_desired_state_name=props.storage_account_desired_state_name, storage_account_data_private_user_name=props.storage_account_data_private_user_name, storage_account_data_private_sensitive_name=props.storage_account_data_private_sensitive_name, From d62c0ccea190a76639b0101cb19cc2281677c01b Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Wed, 18 Sep 2024 12:35:06 +0100 Subject: [PATCH 36/37] Correct template names --- .../resources/workspace/ansible/desired_state.yaml | 6 +++--- .../templates/etc/R/{Rprofile.site => Rprofile.site.j2} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename data_safe_haven/resources/workspace/ansible/templates/etc/R/{Rprofile.site => Rprofile.site.j2} (100%) diff --git a/data_safe_haven/resources/workspace/ansible/desired_state.yaml b/data_safe_haven/resources/workspace/ansible/desired_state.yaml index 3361ffe760..e70bb1c54e 100644 --- a/data_safe_haven/resources/workspace/ansible/desired_state.yaml +++ b/data_safe_haven/resources/workspace/ansible/desired_state.yaml @@ -335,9 +335,9 @@ mode: '0444' loop: - src: etc/pip.conf.j2 - dest: /etc/ - - src: etc/R/Rprofile.site - dest: /etc/R/ + dest: /etc/pip.conf + - src: etc/R/Rprofile.site.j2 + dest: /etc/R/Rprofile.site - name: Copy smoke test files directory ansible.builtin.copy: diff --git a/data_safe_haven/resources/workspace/ansible/templates/etc/R/Rprofile.site b/data_safe_haven/resources/workspace/ansible/templates/etc/R/Rprofile.site.j2 similarity index 100% rename from data_safe_haven/resources/workspace/ansible/templates/etc/R/Rprofile.site rename to data_safe_haven/resources/workspace/ansible/templates/etc/R/Rprofile.site.j2 From 9e6b1b9c8fd162124a6f2e502f431c39043b4326 Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Thu, 19 Sep 2024 09:10:51 +0100 Subject: [PATCH 37/37] Rename class --- data_safe_haven/infrastructure/components/__init__.py | 4 ++-- data_safe_haven/infrastructure/components/wrapped/__init__.py | 4 ++-- .../components/wrapped/nfsv3_storage_account.py | 2 +- data_safe_haven/infrastructure/programs/sre/data.py | 4 ++-- data_safe_haven/infrastructure/programs/sre/desired_state.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data_safe_haven/infrastructure/components/__init__.py b/data_safe_haven/infrastructure/components/__init__.py index 89a127b8d8..7531491bf8 100644 --- a/data_safe_haven/infrastructure/components/__init__.py +++ b/data_safe_haven/infrastructure/components/__init__.py @@ -21,8 +21,8 @@ SSLCertificateProps, ) from .wrapped import ( - NFSV3StorageAccount, WrappedLogAnalyticsWorkspace, + WrappedNFSV3StorageAccount, ) __all__ = [ @@ -39,7 +39,7 @@ "MicrosoftSQLDatabaseProps", "NFSV3BlobContainerComponent", "NFSV3BlobContainerProps", - "NFSV3StorageAccount", + "WrappedNFSV3StorageAccount", "PostgresqlDatabaseComponent", "PostgresqlDatabaseProps", "SSLCertificate", diff --git a/data_safe_haven/infrastructure/components/wrapped/__init__.py b/data_safe_haven/infrastructure/components/wrapped/__init__.py index 48c5570168..ef6e7374d2 100644 --- a/data_safe_haven/infrastructure/components/wrapped/__init__.py +++ b/data_safe_haven/infrastructure/components/wrapped/__init__.py @@ -1,7 +1,7 @@ from .log_analytics_workspace import WrappedLogAnalyticsWorkspace -from .nfsv3_storage_account import NFSV3StorageAccount +from .nfsv3_storage_account import WrappedNFSV3StorageAccount __all__ = [ - "NFSV3StorageAccount", + "WrappedNFSV3StorageAccount", "WrappedLogAnalyticsWorkspace", ] diff --git a/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py b/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py index 0787970fef..181839e71d 100644 --- a/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py +++ b/data_safe_haven/infrastructure/components/wrapped/nfsv3_storage_account.py @@ -6,7 +6,7 @@ from data_safe_haven.external import AzureIPv4Range -class NFSV3StorageAccount(storage.StorageAccount): +class WrappedNFSV3StorageAccount(storage.StorageAccount): encryption_args = storage.EncryptionArgs( key_source=storage.KeySource.MICROSOFT_STORAGE, services=storage.EncryptionServicesArgs( diff --git a/data_safe_haven/infrastructure/programs/sre/data.py b/data_safe_haven/infrastructure/programs/sre/data.py index ede49ba62c..9e18666277 100644 --- a/data_safe_haven/infrastructure/programs/sre/data.py +++ b/data_safe_haven/infrastructure/programs/sre/data.py @@ -31,9 +31,9 @@ from data_safe_haven.infrastructure.components import ( NFSV3BlobContainerComponent, NFSV3BlobContainerProps, - NFSV3StorageAccount, SSLCertificate, SSLCertificateProps, + WrappedNFSV3StorageAccount, ) from data_safe_haven.types import AzureDnsZoneNames @@ -460,7 +460,7 @@ def __init__( # Deploy sensitive data blob storage account # - This holds the /mnt/input and /mnt/output containers that are mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer - storage_account_data_private_sensitive = NFSV3StorageAccount( + storage_account_data_private_sensitive = WrappedNFSV3StorageAccount( f"{self._name}_storage_account_data_private_sensitive", # Storage account names have a maximum of 24 characters account_name=alphanumeric( diff --git a/data_safe_haven/infrastructure/programs/sre/desired_state.py b/data_safe_haven/infrastructure/programs/sre/desired_state.py index 94ccf2b737..73466d6c5b 100644 --- a/data_safe_haven/infrastructure/programs/sre/desired_state.py +++ b/data_safe_haven/infrastructure/programs/sre/desired_state.py @@ -31,7 +31,7 @@ from data_safe_haven.infrastructure.components import ( NFSV3BlobContainerComponent, NFSV3BlobContainerProps, - NFSV3StorageAccount, + WrappedNFSV3StorageAccount, ) from data_safe_haven.resources import resources_path from data_safe_haven.types import AzureDnsZoneNames @@ -102,7 +102,7 @@ def __init__( # Deploy desired state storage account # - This holds the /var/local/ansible container that is mounted by workspaces # - Azure blobs have worse NFS support but can be accessed with Azure Storage Explorer - storage_account = NFSV3StorageAccount( + storage_account = WrappedNFSV3StorageAccount( f"{self._name}_storage_account", account_name=alphanumeric( f"{''.join(truncate_tokens(stack_name.split('-'), 11))}desiredstate{sha256hash(self._name)}"