diff --git a/docs/posts/JHub-User-Guide.html b/docs/JHub-User-Guide.html similarity index 69% rename from docs/posts/JHub-User-Guide.html rename to docs/JHub-User-Guide.html index 2ffe813..c1082a0 100644 --- a/docs/posts/JHub-User-Guide.html +++ b/docs/JHub-User-Guide.html @@ -2,7 +2,7 @@ - + @@ -24,39 +24,33 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -84,10 +77,10 @@ - - - @@ -98,8 +91,8 @@ +
@@ -190,10 +183,8 @@

JHub User Instructions

- -

I have set us up a JupyterHub/RStudio cloud-computing hub on Azure. It’s on Kubernetes and will spin up VMs as needed. The VMs are not huge: 2CPU & 8 Gig RAM.

https://jhub.opensci.live/hub/login

Login authentication is via GitHub. Only members of the JHub GitHub team can log on. This is a testing environment. Contact Eli if you want to test it out.

@@ -319,33 +310,6 @@

Stop your server

} } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -412,15 +376,13 @@

Stop your server

return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -499,9 +461,10 @@

Stop your server

// clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -511,17 +474,8 @@

Stop your server

interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -535,128 +489,6 @@

Stop your server

const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -699,7 +531,6 @@

Stop your server

} div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -725,32 +556,6 @@

Stop your server

}); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -812,12 +617,12 @@

Stop your server

@@ -222,10 +215,8 @@

Set-up CentOS https

- -

Now that our basic JupyterHub is running, we want to secure it. We are going to use Let’s Encrypt. Prerequisites:

  • We need to have set up a domain (URL) that points to the public IP of our JupyterHub
  • @@ -248,7 +239,7 @@

    Create a DNS entry

    Test if the url is working

    -

    http://dhub.bluemountain123.live:8000 would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:

    +

    http:\\dhub.bluemountain123.live:8000 would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:

    @@ -342,7 +333,7 @@

    Restart and Test

    sudo systemctl start jupyterhub.service
    -

    Try https://dhub.bluemountain123.live and you should see the JupyterHub login without the http warning.

    +

    Try https:\\dhub.bluemountain123.live and you should see the JupyterHub login without the http warning.

    @@ -392,33 +383,6 @@

    Restart and Test

    } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -485,15 +449,13 @@

    Restart and Test

    return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -572,9 +534,10 @@

    Restart and Test

    // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -584,17 +547,8 @@

    Restart and Test

    interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -608,128 +562,6 @@

    Restart and Test

    const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -772,7 +604,6 @@

    Restart and Test

    } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -798,32 +629,6 @@

    Restart and Test

    }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -885,12 +690,12 @@

    Restart and Test

    @@ -230,10 +223,8 @@

    Centos Set-up with TLJH

    - -

    This is my notes for setting this up on a Centos 8 (Linux distribution) server. Jump to the “Summary” section to see only the instructions without explanations.

    All the commands are run in a shell (bash)

    References:

    @@ -880,33 +871,6 @@

    Summary

    } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -973,15 +937,13 @@

    Summary

    return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -1060,9 +1022,10 @@

    Summary

    // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -1072,17 +1035,8 @@

    Summary

    interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -1096,128 +1050,6 @@

    Summary

    const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -1260,7 +1092,6 @@

    Summary

    } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -1286,32 +1117,6 @@

    Summary

    }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -1377,9 +1182,7 @@

    Summary

    - + @@ -1388,5 +1191,4 @@

    Summary

    - \ No newline at end of file diff --git a/docs/posts/Set-up-centos.html b/docs/Set-up-centos.html similarity index 84% rename from docs/posts/Set-up-centos.html rename to docs/Set-up-centos.html index e6ae1a5..a7a57b8 100644 --- a/docs/posts/Set-up-centos.html +++ b/docs/Set-up-centos.html @@ -2,7 +2,7 @@ - + @@ -23,7 +23,7 @@ } /* CSS for syntax highlighting */ pre > code.sourceCode { white-space: pre; position: relative; } -pre > code.sourceCode > span { line-height: 1.25; } +pre > code.sourceCode > span { display: inline-block; line-height: 1.25; } pre > code.sourceCode > span:empty { height: 1.2em; } .sourceCode { overflow: visible; } code.sourceCode > span { color: inherit; text-decoration: inherit; } @@ -58,39 +58,33 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -118,10 +111,10 @@ - - - @@ -132,8 +125,8 @@ +
    @@ -232,10 +225,8 @@

    Centos Set-up

    - -

    This is my notes for setting this up on a Centos 8 (Linux distribution) server. Jump to the “Summary” section to see only the instructions without explanations.

    All the commands are run in a shell (bash)

    References:

    @@ -657,7 +648,7 @@

    Creating a shared

    One read-only shared volume:

    https://github.com/jupyterhub/dockerspawner/issues/172

    -
    c.DockerSpawner.volumes = {  'jupyterhub-{username}':'/home/jovyan',   '/path/to/shared': {"bind": '/home/jovyan/shared', "mode": "ro"} }
    +
    c.DockerSpawner.volumes = {  'jupyterhub-user-{username}':'/home/jovyan',   '/path/to/shared': {"bind": '/home/jovyan/shared', "mode": "ro"} }

    A volume that is read-only for some and read-write for others:

    https://github.com/jupyterhub/dockerspawner/issues/172

    @@ -668,7 +659,7 @@

    Creating a shared

    Setting up https

    If you are using a public IP address, rather than being on a private network, you need to set up https so that content (passwords and everything else) is not visible. Read how to do that here.

    -

    These instructions set up this url: https://dhub.bluemountain123.live

    +

    These instructions set up this url: https:\\dhub.bluemountain123.live

    GitHub authentication

    @@ -688,43 +679,15 @@

    C

    Create a team in your GitHub organization

    You will be added by default and add anyone else who needs access to the hub. Let’s say your GitHub organization is MyOrg and the team is called JHub. So then the allowed organization is MyOrg:JHub. You can leave off :JHub if you want to allow all members of the organization to log in.

    -
    -

    Install

    -

    Install the oauthenticator package. Make sure you are in the jupyterhub conda environment.

    -
    -
    # check what environment you are in and switch if needed
    -# conda env list
    -# conda activate jupyterhub
    -conda install -c conda-forge oauthenticator
    -
    -

    Edit the jupyterhub_config.py file

    -

    Edit with something like

    -
    -
    cd /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/
    -nano jupyterhub_config.py
    -
    -

    Add these info. Replace the id, secret, url and admin user with your values. Adding an admin user is handy because then you can do some basic management of the hub. Read more here.

    c.JupyterHub.authenticator_class = "github"
    -c.OAuthenticator.oauth_callback_url = "https://dhub.bluemountain123.live/hub/oauth_callback"
    +c.OAuthenticator.oauth_callback_url = "https:\\dhub.bluemountain123.live/hub/oauth_callback"
     c.OAuthenticator.client_id = "your oauth2 application id"
     c.OAuthenticator.client_secret = "your oauth2 application secret"
    -c.GitHubOAuthenticator.allowed_organizations = {"MyOrg:JHub"}
    -c.GitHubOAuthenticator.scope = ["read:org"]
    -c.GitHubOAuthenticator.admin_users = {"eeholmes"}
    -
    -
    -

    Restart the hub

    -
    -
    sudo systemctl stop jupyterhub.service
    -sudo systemctl start jupyterhub.service
    -
    -

    Now any member you add to the GitHub organization team should be able to log in.

    -

    If you run into trouble, try

    -
    -
    sudo systemctl status jupyterhub.service
    -
    +c.GitHubOAuthenticator.allowed_organizations = "MyOrg:JHub" +c.GitHubOAuthenticator.scope = "read:org" +

    Replace the id, secret and url with your values.

    @@ -732,120 +695,120 @@

    Summary

    Only the instructions. Make sure you are installing as the root user. I assume you have Python and conda installed.

    Create the conda environment

    -
    sudo -i
    -
    -conda create -n jupyterhub python --yes
    -conda activate jupyterhub
    -conda install -c conda-forge jupyterhub --yes
    -conda install -c conda-forge jupyterlab notebook --yes
    -
    -JHUBENV=/opt/miniconda3/envs/jupyterhub
    -chmod 755 $JHUBENV
    +
    sudo -i
    +
    +conda create -n jupyterhub python --yes
    +conda activate jupyterhub
    +conda install -c conda-forge jupyterhub --yes
    +conda install -c conda-forge jupyterlab notebook --yes
    +
    +JHUBENV=/opt/miniconda3/envs/jupyterhub
    +chmod 755 $JHUBENV

    Create user

    -
    useradd jhub
    +
    useradd jhub

    Open the 8000 port for access to the application.

    -
    #sudo systemctl enable firewalld
    -#sudo systemctl start firewalld
    -
    -sudo firewall-cmd  --permanent --add-port 8000/tcp
    -sudo firewall-cmd --reload
    -sudo firewall-cmd --list-ports
    +
    #sudo systemctl enable firewalld
    +#sudo systemctl start firewalld
    +
    +sudo firewall-cmd  --permanent --add-port 8000/tcp
    +sudo firewall-cmd --reload
    +sudo firewall-cmd --list-ports

    Create the configuration file. Will be edited at end.

    -
    sudo mkdir -p $JHUBENV/etc/jupyterhub/
    -cd $JHUBENV/etc/jupyterhub/
    -sudo $JHUBENV/bin/jupyterhub --generate-config
    +
    sudo mkdir -p $JHUBENV/etc/jupyterhub/
    +cd $JHUBENV/etc/jupyterhub/
    +sudo $JHUBENV/bin/jupyterhub --generate-config

    Install docker if needed

    -
    sudo yum install -y yum-utils
    -sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    -
    -sudo systemctl start docker
    +
    sudo yum install -y yum-utils
    +sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    +
    +sudo systemctl start docker

    Not sure this is needed.

    -
    sudo firewall-cmd --zone=docker --add-port=8081/tcp
    -sudo firewall-cmd --reload
    -sudo systemctl restart docker
    +
    sudo firewall-cmd --zone=docker --add-port=8081/tcp
    +sudo firewall-cmd --reload
    +sudo systemctl restart docker

    Install dockerspawner

    -
    conda install -c conda-forge dockerspawner --yes
    -conda install -c conda-forge docker-py --yes
    +
    conda install -c conda-forge dockerspawner --yes
    +conda install -c conda-forge docker-py --yes

    Edit the configuration file.

    -
    cd $JHUBENV/etc/jupyterhub/
    -nano jupyterhub_config.py
    +
    cd $JHUBENV/etc/jupyterhub/
    +nano jupyterhub_config.py

    Paste this in

    -
    # Configuration file for jupyterhub.
    -
    -c = get_config()  #noqa
    -c.JupyterHub.port = 8000
    -c.JupyterHub.hub_bind_url = "http://0.0.0.0:8081"
    -c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
    -c.DockerSpawner.remove = True
    -c.Spawner.http_timeout = 3600
    -c.DockerSpawner.image_whitelist = {
    -    'iorocker': 'eeholmes/iorocker-standalone:20231003',
    -    'rocker-binder': 'eeholmes/rocker-binder:20231003',
    -    'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',
    -    'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',
    -    'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',
    -}
    -
    -notebook_dir = '/home/jovyan'
    -c.DockerSpawner.notebook_dir = notebook_dir
    -
    -# Mount the real user's Docker volume on the host to the notebook user's
    -# notebook directory in the container
    -c.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }
    +
    # Configuration file for jupyterhub.
    +
    +c = get_config()  #noqa
    +c.JupyterHub.port = 8000
    +c.JupyterHub.hub_bind_url = "http://0.0.0.0:8081"
    +c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
    +c.DockerSpawner.remove = True
    +c.Spawner.http_timeout = 3600
    +c.DockerSpawner.image_whitelist = {
    +    'iorocker': 'eeholmes/iorocker-standalone:20231003',
    +    'rocker-binder': 'eeholmes/rocker-binder:20231003',
    +    'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',
    +    'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',
    +    'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',
    +}
    +
    +notebook_dir = '/home/jovyan'
    +c.DockerSpawner.notebook_dir = notebook_dir
    +
    +# Mount the real user's Docker volume on the host to the notebook user's
    +# notebook directory in the container
    +c.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }

    Docker pull of the images. Do all.

    -
    docker pull jupyter/datascience-notebook:r-4.3.1
    -docker pull jupyter/scipy-notebook:7e1a19a8427f
    +
    docker pull jupyter/datascience-notebook:r-4.3.1
    +docker pull jupyter/scipy-notebook:7e1a19a8427f

    Make a new server service

    -
    sudo mkdir -p $JHUBENV/etc/systemd
    -cd $JHUBENV/etc/systemd
    -nano jupyterhub.service
    +
    sudo mkdir -p $JHUBENV/etc/systemd
    +cd $JHUBENV/etc/systemd
    +nano jupyterhub.service

    Paste this in

    -
    [Unit]
    -Description=JupyterHub
    -After=syslog.target network.target
    -
    -[Service]
    -User=root
    -Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin"
    -ExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py
    -
    -[Install]
    -WantedBy=multi-user.target
    +
    [Unit]
    +Description=JupyterHub
    +After=syslog.target network.target
    +
    +[Service]
    +User=root
    +Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin"
    +ExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py
    +
    +[Install]
    +WantedBy=multi-user.target

    Make sure SELinux doesn’t block our service

    -
    ls -Z $JHUBENV/etc/systemd/
    -sudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service
    -sudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \;
    +
    ls -Z $JHUBENV/etc/systemd/
    +sudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service
    +sudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \;

    Enable our new service

    -
    sudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service
    -sudo systemctl daemon-reload
    -sudo systemctl enable jupyterhub.service
    -sudo systemctl start jupyterhub.service
    +
    sudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service
    +sudo systemctl daemon-reload
    +sudo systemctl enable jupyterhub.service
    +sudo systemctl start jupyterhub.service

    Done! See the long instructions if anything is not working.

    Now go through the https and GitHub authentication steps if you need that.

    @@ -897,33 +860,6 @@

    Summary

    } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -990,15 +926,13 @@

    Summary

    return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -1077,9 +1011,10 @@

    Summary

    // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -1089,17 +1024,8 @@

    Summary

    interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -1113,128 +1039,6 @@

    Summary

    const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -1277,7 +1081,6 @@

    Summary

    } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -1303,32 +1106,6 @@

    Summary

    }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -1390,12 +1167,12 @@

    Summary

    @@ -195,10 +188,8 @@

    DaskHub Set-up

    - -

    This is my notes for setting this up on Azure. Attempting to replicate the Openscapes 2i2c JupyterHub: https://github.com/2i2c-org/infrastructure/tree/master/config/clusters/openscapes

    That hub is on AWS and is designed for large workshops (100+) however the NMFS OpenSci JHub is quite similar. Main difference at the moment is that I don’t have a shared drive set-up and the user persistent volume (storage) is on the same VM as the user node for their Jupyter Notebook. This means that I cannot have multiple VM sizes. Need to fix so that user can pick a larger VM for a task if needed.

    @@ -314,7 +305,7 @@

    Update

    Test if https is working

    -

    Try https:\\dhub.bluemountain123.live and you should see the JupyterHub login without that http warning. This might take 30 minutes to an hour.

    +

    Try https:\\dhub.bluemountain123.live and you should see the JupyterHub login without that http warning.

    @@ -581,33 +572,6 @@

    S3 access

    } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -674,15 +638,13 @@

    S3 access

    return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -761,9 +723,10 @@

    S3 access

    // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -773,17 +736,8 @@

    S3 access

    interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -797,128 +751,6 @@

    S3 access

    const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -961,7 +793,6 @@

    S3 access

    } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -987,32 +818,6 @@

    S3 access

    }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -1074,12 +879,12 @@

    S3 access

    -

    Instructions for editing config

      @@ -237,33 +229,6 @@

      Instructions for editing config

      } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -330,15 +295,13 @@

      Instructions for editing config

      return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -417,9 +380,10 @@

      Instructions for editing config

      // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -429,17 +393,8 @@

      Instructions for editing config

      interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -453,128 +408,6 @@

      Instructions for editing config

      const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -617,7 +450,6 @@

      Instructions for editing config

      } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -643,32 +475,6 @@

      Instructions for editing config

      }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -734,9 +540,7 @@

      Instructions for editing config

      - + @@ -745,5 +549,4 @@

      Instructions for editing config

      - \ No newline at end of file diff --git a/docs/ci/arcgis/instructions.html b/docs/ci/arcgis/instructions.html deleted file mode 100644 index d301441..0000000 --- a/docs/ci/arcgis/instructions.html +++ /dev/null @@ -1,745 +0,0 @@ - - - - - - - - - -Eli's JupyterHub notes – instructions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      -
      - -
      - -
      - - -
      - - - -
      - - - - -
      -

      Testing

      -

      This was testing. Doesn’t work. Discovered that

      -
        -
      • jupyter-server-proxy is needed
      • -
      • works in docker but not in jupyterhub I think due to the server app being different. I tried to change to old server but no luck.
      • -
      • doesn’t work with the juptyer/base-notebook:python-3.9. Everything I tried failed.
      • -
      -
      -
      -

      tldr;

      -
      cd ci/arcgis
      -DOCKER_TAG="20240129b"
      -docker build --platform linux/amd64 -t eeholmes/py-rocket-gis:${DOCKER_TAG} .
      -docker push eeholmes/py-rocket-gis:${DOCKER_TAG}
      -

      Testing

      -
      DOCKER_TAG="$(git rev-parse --short HEAD)"
      -docker build --platform linux/amd64 -t eeholmes/py-rocket-gis:${DOCKER_TAG} .
      -docker push eeholmes/py-rocket-gis:${DOCKER_TAG}
      - - -
      - -
      - -
      -
      - -
      - - - - - \ No newline at end of file diff --git a/docs/ci/iopython-tf/instructions.html b/docs/ci/iopython-tf/instructions.html index b82e587..9990997 100644 --- a/docs/ci/iopython-tf/instructions.html +++ b/docs/ci/iopython-tf/instructions.html @@ -2,7 +2,7 @@ - + @@ -47,13 +47,7 @@ "collapse-after": 3, "panel-placement": "start", "type": "textbox", - "limit": 50, - "keyboard-shortcut": [ - "f", - "/", - "s" - ], - "show-item-context": false, + "limit": 20, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", @@ -63,8 +57,7 @@ "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-detached-cancel-button-title": "Cancel", - "search-submit-button-title": "Submit", - "search-label": "Search" + "search-submit-button-title": "Submit" } } @@ -81,10 +74,10 @@ - - - @@ -117,31 +110,31 @@ @@ -170,14 +163,13 @@

      On this page

    1. Adding packages with newpackages.yml

- +
-

Indian Ocean Summer Docker Images

https://hub.docker.com/repository/docker/eeholmes/iopython-tf/general

@@ -332,33 +324,6 @@

Addin } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -425,15 +390,13 @@

Addin return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -512,9 +475,10 @@

Addin // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -524,17 +488,8 @@

Addin interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -548,128 +503,6 @@

Addin const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -712,7 +545,6 @@

Addin } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -738,32 +570,6 @@

Addin }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -829,9 +635,7 @@

Addin - + @@ -840,5 +644,4 @@

Addin - \ No newline at end of file diff --git a/docs/ci/iopython/instructions.html b/docs/ci/iopython/instructions.html index 46a27d0..71397ae 100644 --- a/docs/ci/iopython/instructions.html +++ b/docs/ci/iopython/instructions.html @@ -2,7 +2,7 @@ - + @@ -47,13 +47,7 @@ "collapse-after": 3, "panel-placement": "start", "type": "textbox", - "limit": 50, - "keyboard-shortcut": [ - "f", - "/", - "s" - ], - "show-item-context": false, + "limit": 20, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", @@ -63,8 +57,7 @@ "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-detached-cancel-button-title": "Cancel", - "search-submit-button-title": "Submit", - "search-label": "Search" + "search-submit-button-title": "Submit" } } @@ -81,10 +74,10 @@ - - - @@ -117,31 +110,31 @@ @@ -170,14 +163,13 @@

On this page

  • Adding packages with newpackages.yml
  • - +
    -

    Indian Ocean Summer Docker Images: Openscapes + a few extras

    https://hub.docker.com/repository/docker/eeholmes/iopython/general

    @@ -334,33 +326,6 @@

    Addin } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -427,15 +392,13 @@

    Addin return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -514,9 +477,10 @@

    Addin // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -526,17 +490,8 @@

    Addin interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -550,128 +505,6 @@

    Addin const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -714,7 +547,6 @@

    Addin } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -740,32 +572,6 @@

    Addin }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -831,9 +637,7 @@

    Addin - + @@ -842,5 +646,4 @@

    Addin - \ No newline at end of file diff --git a/docs/ci/iorocker/instructions.html b/docs/ci/iorocker/instructions.html index 53bdc18..79bb4c3 100644 --- a/docs/ci/iorocker/instructions.html +++ b/docs/ci/iorocker/instructions.html @@ -2,7 +2,7 @@ - + @@ -47,13 +47,7 @@ "collapse-after": 3, "panel-placement": "start", "type": "textbox", - "limit": 50, - "keyboard-shortcut": [ - "f", - "/", - "s" - ], - "show-item-context": false, + "limit": 20, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", @@ -63,8 +57,7 @@ "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-detached-cancel-button-title": "Cancel", - "search-submit-button-title": "Submit", - "search-label": "Search" + "search-submit-button-title": "Submit" } } @@ -81,10 +74,10 @@ - - - @@ -117,31 +110,31 @@ @@ -170,14 +163,13 @@

    On this page

  • Adding packages with newpackages.yml
  • - +
    -

    Indian Ocean Summer Docker Images

    https://hub.docker.com/repository/docker/eeholmes/iorocker/general

    @@ -332,33 +324,6 @@

    Addin } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -425,15 +390,13 @@

    Addin return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -512,9 +475,10 @@

    Addin // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -524,17 +488,8 @@

    Addin interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -548,128 +503,6 @@

    Addin const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -712,7 +545,6 @@

    Addin } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -738,32 +570,6 @@

    Addin }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -829,9 +635,7 @@

    Addin - + @@ -840,5 +644,4 @@

    Addin - \ No newline at end of file diff --git a/docs/ci/iosdmTMB/instructions.html b/docs/ci/iosdmTMB/instructions.html index a48587d..d4e9b90 100644 --- a/docs/ci/iosdmTMB/instructions.html +++ b/docs/ci/iosdmTMB/instructions.html @@ -2,7 +2,7 @@ - + @@ -47,13 +47,7 @@ "collapse-after": 3, "panel-placement": "start", "type": "textbox", - "limit": 50, - "keyboard-shortcut": [ - "f", - "/", - "s" - ], - "show-item-context": false, + "limit": 20, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", @@ -63,8 +57,7 @@ "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-detached-cancel-button-title": "Cancel", - "search-submit-button-title": "Submit", - "search-label": "Search" + "search-submit-button-title": "Submit" } } @@ -81,10 +74,10 @@ - - - @@ -117,31 +110,31 @@ @@ -170,14 +163,13 @@

    On this page

  • Adding packages with newpackages.yml
  • - +
    -

    Indian Ocean Summer Docker Images

    https://hub.docker.com/repository/docker/eeholmes/iosdmTMB/general

    @@ -333,33 +325,6 @@

    Addin } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -426,15 +391,13 @@

    Addin return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -513,9 +476,10 @@

    Addin // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -525,17 +489,8 @@

    Addin interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -549,128 +504,6 @@

    Addin const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -713,7 +546,6 @@

    Addin } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -739,32 +571,6 @@

    Addin }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -830,9 +636,7 @@

    Addin - + @@ -841,5 +645,4 @@

    Addin - \ No newline at end of file diff --git a/docs/ci/py-rocker-base/instructions.html b/docs/ci/py-rocker-base/instructions.html deleted file mode 100644 index f1c938b..0000000 --- a/docs/ci/py-rocker-base/instructions.html +++ /dev/null @@ -1,747 +0,0 @@ - - - - - - - - - -Eli's JupyterHub notes – instructions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - -
    - -
    - - -
    - - - -
    - - - - -
    -

    py-rocket-base

    -

    This is based on the openscapes/py-rocket but simplified.

    -

    https://hub.docker.com/repository/docker/eeholmes/py-rocket-base/general

    -

    The one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes.

    -
    -
    -

    tldr;

    -
    cd ci/py-rocket-base
    -DOCKER_TAG="20230901"
    -docker build --platform linux/amd64 -t eeholmes/py-rocket-base:${DOCKER_TAG} -t eeholmes/py-rocket-base:main .
    -docker push eeholmes/py-rocket-base:${DOCKER_TAG}
    -docker push eeholmes/py-rocket-base:main
    -

    Testing

    -
    DOCKER_TAG="$(git rev-parse --short HEAD)"
    -docker build --platform linux/amd64 -t eeholmes/py-rocket-base:${DOCKER_TAG} .
    -docker push eeholmes/py-rocket-base:${DOCKER_TAG}
    -

    Log into Azure portal, go to DaskHub, Connect, Cloud Shell, and edit dconfig2.yaml (nano dconfig2.yaml) to update the image tag. Then run this command.

    -
    helm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml
    -

    Tip: if things fill up use

    -
    docker system prune --all
    - - -
    - -
    - -
    -
    - -
    - - - - - \ No newline at end of file diff --git a/docs/ci/py-rocket/instructions.html b/docs/ci/py-rocket/instructions.html deleted file mode 100644 index a1a91e6..0000000 --- a/docs/ci/py-rocket/instructions.html +++ /dev/null @@ -1,852 +0,0 @@ - - - - - - - - - -Eli's JupyterHub notes – instructions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - -
    - -
    - - -
    - - - -
    - - - - -
    -

    This is based on the openscapes/py-rocket

    -

    https://hub.docker.com/repository/docker/eeholmes/py-rocket/general

    -

    The one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes.

    -
    -
    -

    tldr;

    -
    cd ci/py-rocket
    -DOCKER_TAG="20230901"
    -docker build --platform linux/amd64 -t eeholmes/py-rocket:${DOCKER_TAG} -t eeholmes/py-rocket:main .
    -docker push eeholmes/py-rocket:${DOCKER_TAG}
    -docker push eeholmes/py-rocket:main
    -

    Log into Azure portal, go to DaskHub, Connect, Cloud Shell, and edit dconfig2.yaml (nano dconfig2.yaml) to update the py-rocket tag. Then run this command.

    -
    helm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml
    -

    Tip: if things fill up use

    -
    docker system prune --all
    -
    -
    -

    Requirements

    -

    Docker installed. For example, if doing on a Mac or PC, you need Docker Desktop. On VMs, docker will already be installed.

    -

    A DockerHub user account. The instructions are using EEH’s.

    -
    -
    -

    Add new packages

    -

    Add to Dockerfile something like

    -
    RUN R -e 'install.packages("gtools", repos = "http://cran.us.r-project.org")'
    -
    -
    -

    Rebuild and push the Docker image

    -
      -
    1. Make sure Docker app is running, not just installed. So if you are on a local computer, start up the app (open it).
    2. -
    3. Go to a terminal and cd to the directory with the Dockerfile. So to the ci directory in the nmfs-jhub repo.
    4. -
    -
    cd ci/py-rocket
    -
      -
    1. Update the docker tag to the date.
    2. -
    -
    DOCKER_TAG="20230901"
    -
      -
    1. Build the image. . means current directory. eeholmes/py-rocket is the name of the repo on DockerHub. --platform is added if you are building on an Mac with Apple chip.
    2. -
    -
    docker build --platform linux/amd64 -t eeholmes/py-rocket:${DOCKER_TAG} -t eeholmes/py-rocket:main .
    -
      -
    1. Push the image up to DockerHub. Make sure you are logged into DockerHub in the Docker app otherwise you’ll get “access denied”. Open the Docker app and look that it shows that you are signed in.
    2. -
    -
    docker push eeholmes/py-rocket:${DOCKER_TAG}
    -docker push eeholmes/py-rocket:main
    -

    Notes: https://help.valohai.com/hc/en-us/articles/4421364087569-Build-your-own-Docker-image

    -
    -
    -

    Stop any running the Jupyter Lab instances

    -

    Log in. File > Hub Control > Stop my server

    -
    -
    -

    Run helm upgrade

    -

    Log into Azure portal, go to DaskHub, Connect, Cloud Shell, and run this command. Note, this assumes that eeholmes/iorocker:main is still the image to use. If not, edit dconfig2.yaml (nano dconfig2.yaml) and then upgrade.

    -
    helm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml
    -
    -

    If an specific image tag is in config

    -

    The JupyterHub has a config file that specifies what images are being used. If the image is say eeholmes/iopython:hublatest, then whenever the a image with tag hublatest is pushed, the hub will use that. If on the otherhand, you config file has a specific, an unique tag that you don’t overwrite, then you’ll have to update the file in the config file on the cluster (log into Azure, go to cluster, connect to cloud shell, nano dconfig2.yaml) and upgrade the installation of the JupyterHub.

    -

    Why not eeholmes/iopython:latest? There is nothing special about latest. It is the default tag used if you don’t specify -t and : in your build call. So it is a bit too easy to accidentally update “latest” and thus update the image for you hub when you didn’t intend to do that. You just forgot to specify a tag.

    -

    To update if you are using a specific tag, like 20230615 rather than one you keep updating like hublatest or latest:

    -
    -

    Step 1

    -

    Edit the config file. Mine is called dconfig2.yaml. Yours is probably config.yaml. Name is unimportant.

    -
    nano dconfig2.yaml
    -

    Inside dconfig2.yaml is this info. This shows a fixed tag. So if I update, I need to change the 20230615 part.

    -
      singleuser:
    -    image:
    -      name: eeholmes/iopython
    -      tag: 20230615
    -

    Save the changes. In nano, it cmd-O, return, cmd-X.

    -
    -
    -

    run helm upgrade

    -
    helm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml
    -
    -
    -

    The helm upgrade command

    -
    helm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml
    -

    A helm is what runs the commands to upgrade (and install in the beginning) our JupyterHub. dask/daskhub is point to the repo with the “helm chart” (the instructions). --value dconfig2.yaml is telling it where the config file is.

    -
      -
    • upgrade upgrade an existing installation with the values in dconfig2.yaml
    • -
    • --render-subchart-notes the dask/daskhub helm chart has subcharts (jupyterhub) and you need to render these too. Not all helm charts have this.
    • -
    • dask/daskhub the name of the repo that has the helm chart. The first time you reference this, you need to tell help about the repo by giving it the url. Read how here
    • -
    • --version=2023.1.0 version of the helm chart. Update when the helm chart (instructions for installing the jupyterhub) changes.
    • -
    -
    -
    -
    -

    Adding packages with newpackages.yml

    -

    When the openscapes image is used, we are in a conda env called ‘notebook’. We want to update that with the packages in newpackages.yml but need to get that file into the container. For now, I just hard code in the pip install commands.

    -

    Add to Docker file

    -
    # it can't find new.yml in home/joyvan/.kernels
    -# need to get that into the container somehow (git clone?)
    -# RUN conda env update --file new.yml
    -
    -# Asides
    -
    -## To set docker tag to latest commit
    -
    -

    SHA7=“\((git rev-parse --short HEAD)" DOCKER_TAG=\)SHA7

    -
    I am not doing that since this repo has lots of commits unrelated to the docker image.
    -
    -## To set up your own Docker repo
    -
    -1. Make an account on DockerHub. Free is fine.
    -2. Create a repo and give it a name. For example, for this project, my account is `eeholmes` and my repo is `iopython` (Indian Ocean Python) as it is specific a particular project I am working on.
    -
    -DockerHub will want to you to buy the premium account but you only need that if you are doing continuous integration, like using a GitHub Action to autobuild your image. If you are manually building, you don't need this.
    -
    -## Why is `--platform` needed in the build command
    -
    -You won't see this on docker build tutorials. But if you are on a Mac with Apple chip, then you'll build arm64 images and that's not going to work on Ubuntu VMs. The vanilla images you see are amd64 so we want to make sure we are building for that platform. This only matters if you are on a Mac with Apple chip, but it won't break things for unix and PC so I added to make the instructions more robust.
    - - -
    -
    - -
    - -
    -
    - -
    - - - - - \ No newline at end of file diff --git a/docs/images/img1.png b/docs/images/img1.png new file mode 100644 index 0000000..de091de Binary files /dev/null and b/docs/images/img1.png differ diff --git a/docs/images/img10.png b/docs/images/img10.png new file mode 100644 index 0000000..aae225e Binary files /dev/null and b/docs/images/img10.png differ diff --git a/docs/images/img11.png b/docs/images/img11.png new file mode 100644 index 0000000..cdebc05 Binary files /dev/null and b/docs/images/img11.png differ diff --git a/docs/images/img12.png b/docs/images/img12.png new file mode 100644 index 0000000..def2dc7 Binary files /dev/null and b/docs/images/img12.png differ diff --git a/docs/images/img13.png b/docs/images/img13.png new file mode 100644 index 0000000..ffed094 Binary files /dev/null and b/docs/images/img13.png differ diff --git a/docs/images/img2.png b/docs/images/img2.png new file mode 100644 index 0000000..6cd374a Binary files /dev/null and b/docs/images/img2.png differ diff --git a/docs/images/img3.png b/docs/images/img3.png new file mode 100644 index 0000000..db6571a Binary files /dev/null and b/docs/images/img3.png differ diff --git a/docs/images/img4.png b/docs/images/img4.png new file mode 100644 index 0000000..fff8d56 Binary files /dev/null and b/docs/images/img4.png differ diff --git a/docs/images/img5.png b/docs/images/img5.png new file mode 100644 index 0000000..b4f8e86 Binary files /dev/null and b/docs/images/img5.png differ diff --git a/docs/images/img6.png b/docs/images/img6.png new file mode 100644 index 0000000..c13e145 Binary files /dev/null and b/docs/images/img6.png differ diff --git a/docs/images/img7.png b/docs/images/img7.png new file mode 100644 index 0000000..69ec86b Binary files /dev/null and b/docs/images/img7.png differ diff --git a/docs/images/img8.png b/docs/images/img8.png new file mode 100644 index 0000000..b3a1ce8 Binary files /dev/null and b/docs/images/img8.png differ diff --git a/docs/images/img9.png b/docs/images/img9.png new file mode 100644 index 0000000..cef65fc Binary files /dev/null and b/docs/images/img9.png differ diff --git a/docs/images/user-guide/img1.png b/docs/images/user-guide/img1.png new file mode 100644 index 0000000..19f3137 Binary files /dev/null and b/docs/images/user-guide/img1.png differ diff --git a/docs/images/user-guide/img1b.png b/docs/images/user-guide/img1b.png new file mode 100644 index 0000000..3f3e982 Binary files /dev/null and b/docs/images/user-guide/img1b.png differ diff --git a/docs/images/user-guide/img2.png b/docs/images/user-guide/img2.png new file mode 100644 index 0000000..16f315f Binary files /dev/null and b/docs/images/user-guide/img2.png differ diff --git a/docs/images/user-guide/img3.png b/docs/images/user-guide/img3.png new file mode 100644 index 0000000..13027c5 Binary files /dev/null and b/docs/images/user-guide/img3.png differ diff --git a/docs/images/user-guide/img4.png b/docs/images/user-guide/img4.png new file mode 100644 index 0000000..517f8f9 Binary files /dev/null and b/docs/images/user-guide/img4.png differ diff --git a/docs/images/user-guide/img5.png b/docs/images/user-guide/img5.png new file mode 100644 index 0000000..16777b3 Binary files /dev/null and b/docs/images/user-guide/img5.png differ diff --git a/docs/images/user-guide/img6.png b/docs/images/user-guide/img6.png new file mode 100644 index 0000000..bfc9314 Binary files /dev/null and b/docs/images/user-guide/img6.png differ diff --git a/docs/index.html b/docs/index.html index ba1c856..a638fde 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,7 +2,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -48,13 +48,7 @@ "collapse-after": 3, "panel-placement": "start", "type": "textbox", - "limit": 50, - "keyboard-shortcut": [ - "f", - "/", - "s" - ], - "show-item-context": false, + "limit": 20, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", @@ -64,8 +58,7 @@ "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-detached-cancel-button-title": "Cancel", - "search-submit-button-title": "Submit", - "search-label": "Search" + "search-submit-button-title": "Submit" } } @@ -82,10 +75,10 @@ - - - @@ -118,31 +111,31 @@ @@ -159,7 +152,7 @@

    On this page

  • Test JHub
  • Installation instructions
  • - +
    @@ -179,10 +172,8 @@

    NMFS OpenSci JupyterHub Notes

    - -

    This page shows my notes for setting up JupyterHubs on various platforms. What is a JupyterHub? Read about why cloud computing environments are great: SnowEx 2022

    Test JHub

    @@ -241,33 +232,6 @@

    Installation ins } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -334,15 +298,13 @@

    Installation ins return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -421,9 +383,10 @@

    Installation ins // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -433,17 +396,8 @@

    Installation ins interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -457,128 +411,6 @@

    Installation ins const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -621,7 +453,6 @@

    Installation ins } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -647,32 +478,6 @@

    Installation ins }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -736,7 +541,7 @@

    Installation ins @@ -747,9 +552,7 @@

    Installation ins - + @@ -758,5 +561,4 @@

    Installation ins - \ No newline at end of file diff --git a/docs/search.json b/docs/search.json index 6178cc8..9876c00 100644 --- a/docs/search.json +++ b/docs/search.json @@ -1,632 +1,213 @@ [ { - "objectID": "ci/iopython/instructions.html", - "href": "ci/iopython/instructions.html", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "", - "text": "https://hub.docker.com/repository/docker/eeholmes/iopython/general\nThe one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes." - }, - { - "objectID": "ci/iopython/instructions.html#to-set-docker-tag-to-latest-commit", - "href": "ci/iopython/instructions.html#to-set-docker-tag-to-latest-commit", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "To set docker tag to latest commit", - "text": "To set docker tag to latest commit\nSHA7=\"$(git rev-parse --short HEAD)\"\nDOCKER_TAG=$SHA7\nI am not doing that since this repo has lots of commits unrelated to the docker image." - }, - { - "objectID": "ci/iopython/instructions.html#to-set-up-your-own-docker-repo", - "href": "ci/iopython/instructions.html#to-set-up-your-own-docker-repo", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "To set up your own Docker repo", - "text": "To set up your own Docker repo\n\nMake an account on DockerHub. Free is fine.\nCreate a repo and give it a name. For example, for this project, my account is eeholmes and my repo is iopython (Indian Ocean Python) as it is specific a particular project I am working on.\n\nDockerHub will want to you to buy the premium account but you only need that if you are doing continuous integration, like using a GitHub Action to autobuild your image. If you are manually building, you don’t need this." - }, - { - "objectID": "ci/iopython/instructions.html#why-is---platform-needed-in-the-build-command", - "href": "ci/iopython/instructions.html#why-is---platform-needed-in-the-build-command", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "Why is --platform needed in the build command", - "text": "Why is --platform needed in the build command\nYou won’t see this on docker build tutorials. But if you are on a Mac with Apple chip, then you’ll build arm64 images and that’s not going to work on Ubuntu VMs. The vanilla images you see are amd64 so we want to make sure we are building for that platform. This only matters if you are on a Mac with Apple chip, but it won’t break things for unix and PC so I added to make the instructions more robust." - }, - { - "objectID": "ci/iopython/instructions.html#if-a-specific-image-tag-is-in-config", - "href": "ci/iopython/instructions.html#if-a-specific-image-tag-is-in-config", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "If a specific image tag is in config", - "text": "If a specific image tag is in config\nThe JupyterHub has a config file that specifies what images are being used. If the image is say eeholmes/iopython:hublatest, then whenever the a image with tag hublatest is pushed, the hub will use that. If on the otherhand, you config file has a specific, an unique tag that you don’t overwrite, then you’ll have to update the file in the config file on the cluster (log into Azure, go to cluster, connect to cloud shell, nano dconfig2.yaml) and upgrade the installation of the JupyterHub.\nWhy not eeholmes/iopython:latest? There is nothing special about latest. It is the default tag used if you don’t specify -t and : in your build call. So it is a bit too easy to accidentally update “latest” and thus update the image for you hub when you didn’t intend to do that. You just forgot to specify a tag.\nTo update if you are using a specific tag, like 20230615 rather than one you keep updating like hublatest or latest:\n\nStep 1\nEdit the config file. Mine is called dconfig2.yaml. Yours is probably config.yaml. Name is unimportant.\nnano dconfig2.yaml\nInside dconfig2.yaml is this info. This shows a fixed tag. So if I update, I need to change the 20230615 part.\n singleuser:\n image:\n name: eeholmes/iopython\n tag: 20230615\nSave the changes. In nano, it cmd-O, return, cmd-X.\n\n\nrun helm upgrade\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\n\n\nThe helm upgrade command\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\nA helm is what runs the commands to upgrade (and install in the beginning) our JupyterHub. dask/daskhub is point to the repo with the “helm chart” (the instructions). --value dconfig2.yaml is telling it where the config file is.\n\nupgrade upgrade an existing installation with the values in dconfig2.yaml\n--render-subchart-notes the dask/daskhub helm chart has subcharts (jupyterhub) and you need to render these too. Not all helm charts have this.\ndask/daskhub the name of the repo that has the helm chart. The first time you reference this, you need to tell help about the repo by giving it the url. Read how here\n--version=2023.1.0 version of the helm chart. Update when the helm chart (instructions for installing the jupyterhub) changes." - }, - { - "objectID": "ci/iopython/instructions.html#adding-packages-with-newpackages.yml", - "href": "ci/iopython/instructions.html#adding-packages-with-newpackages.yml", - "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", - "section": "Adding packages with newpackages.yml", - "text": "Adding packages with newpackages.yml\nWhen the openscapes image is used, we are in a conda env called ‘notebook’. We want to update that with the packages in newpackages.yml but need to get that file into the container. For now, I just hard code in the pip/conda install commands.\nAdd to Docker file\n# it can't find new.yml in home/joyvan/.kernels\n# need to get that into the container somehow (git clone?)\n# RUN conda env update --file new.yml" - }, - { - "objectID": "ci/py-rocket/instructions.html", - "href": "ci/py-rocket/instructions.html", - "title": "This is based on the openscapes/py-rocket", - "section": "", - "text": "https://hub.docker.com/repository/docker/eeholmes/py-rocket/general\nThe one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes." - }, - { - "objectID": "ci/py-rocket/instructions.html#if-an-specific-image-tag-is-in-config", - "href": "ci/py-rocket/instructions.html#if-an-specific-image-tag-is-in-config", - "title": "This is based on the openscapes/py-rocket", - "section": "If an specific image tag is in config", - "text": "If an specific image tag is in config\nThe JupyterHub has a config file that specifies what images are being used. If the image is say eeholmes/iopython:hublatest, then whenever the a image with tag hublatest is pushed, the hub will use that. If on the otherhand, you config file has a specific, an unique tag that you don’t overwrite, then you’ll have to update the file in the config file on the cluster (log into Azure, go to cluster, connect to cloud shell, nano dconfig2.yaml) and upgrade the installation of the JupyterHub.\nWhy not eeholmes/iopython:latest? There is nothing special about latest. It is the default tag used if you don’t specify -t and : in your build call. So it is a bit too easy to accidentally update “latest” and thus update the image for you hub when you didn’t intend to do that. You just forgot to specify a tag.\nTo update if you are using a specific tag, like 20230615 rather than one you keep updating like hublatest or latest:\n\nStep 1\nEdit the config file. Mine is called dconfig2.yaml. Yours is probably config.yaml. Name is unimportant.\nnano dconfig2.yaml\nInside dconfig2.yaml is this info. This shows a fixed tag. So if I update, I need to change the 20230615 part.\n singleuser:\n image:\n name: eeholmes/iopython\n tag: 20230615\nSave the changes. In nano, it cmd-O, return, cmd-X.\n\n\nrun helm upgrade\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\n\n\nThe helm upgrade command\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\nA helm is what runs the commands to upgrade (and install in the beginning) our JupyterHub. dask/daskhub is point to the repo with the “helm chart” (the instructions). --value dconfig2.yaml is telling it where the config file is.\n\nupgrade upgrade an existing installation with the values in dconfig2.yaml\n--render-subchart-notes the dask/daskhub helm chart has subcharts (jupyterhub) and you need to render these too. Not all helm charts have this.\ndask/daskhub the name of the repo that has the helm chart. The first time you reference this, you need to tell help about the repo by giving it the url. Read how here\n--version=2023.1.0 version of the helm chart. Update when the helm chart (instructions for installing the jupyterhub) changes." - }, - { - "objectID": "ci/py-rocket/instructions.html#adding-packages-with-newpackages.yml", - "href": "ci/py-rocket/instructions.html#adding-packages-with-newpackages.yml", - "title": "This is based on the openscapes/py-rocket", - "section": "Adding packages with newpackages.yml", - "text": "Adding packages with newpackages.yml\nWhen the openscapes image is used, we are in a conda env called ‘notebook’. We want to update that with the packages in newpackages.yml but need to get that file into the container. For now, I just hard code in the pip install commands.\nAdd to Docker file\n# it can't find new.yml in home/joyvan/.kernels\n# need to get that into the container somehow (git clone?)\n# RUN conda env update --file new.yml\n\n# Asides\n\n## To set docker tag to latest commit\n\nSHA7=“\\((git rev-parse --short HEAD)\" DOCKER_TAG=\\)SHA7\nI am not doing that since this repo has lots of commits unrelated to the docker image.\n\n## To set up your own Docker repo\n\n1. Make an account on DockerHub. Free is fine.\n2. Create a repo and give it a name. For example, for this project, my account is `eeholmes` and my repo is `iopython` (Indian Ocean Python) as it is specific a particular project I am working on.\n\nDockerHub will want to you to buy the premium account but you only need that if you are doing continuous integration, like using a GitHub Action to autobuild your image. If you are manually building, you don't need this.\n\n## Why is `--platform` needed in the build command\n\nYou won't see this on docker build tutorials. But if you are on a Mac with Apple chip, then you'll build arm64 images and that's not going to work on Ubuntu VMs. The vanilla images you see are amd64 so we want to make sure we are building for that platform. This only matters if you are on a Mac with Apple chip, but it won't break things for unix and PC so I added to make the instructions more robust." - }, - { - "objectID": "ci/arcgis/instructions.html", - "href": "ci/arcgis/instructions.html", - "title": "Testing", - "section": "", - "text": "Testing\nThis was testing. Doesn’t work. Discovered that\n\njupyter-server-proxy is needed\nworks in docker but not in jupyterhub I think due to the server app being different. I tried to change to old server but no luck.\ndoesn’t work with the juptyer/base-notebook:python-3.9. Everything I tried failed.\n\n\n\ntldr;\ncd ci/arcgis\nDOCKER_TAG=\"20240129b\"\ndocker build --platform linux/amd64 -t eeholmes/py-rocket-gis:${DOCKER_TAG} .\ndocker push eeholmes/py-rocket-gis:${DOCKER_TAG}\nTesting\nDOCKER_TAG=\"$(git rev-parse --short HEAD)\"\ndocker build --platform linux/amd64 -t eeholmes/py-rocket-gis:${DOCKER_TAG} .\ndocker push eeholmes/py-rocket-gis:${DOCKER_TAG}" - }, - { - "objectID": "ci/py-rocker-base/instructions.html", - "href": "ci/py-rocker-base/instructions.html", - "title": "py-rocket-base", - "section": "", - "text": "py-rocket-base\nThis is based on the openscapes/py-rocket but simplified.\nhttps://hub.docker.com/repository/docker/eeholmes/py-rocket-base/general\nThe one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes.\n\n\ntldr;\ncd ci/py-rocket-base\nDOCKER_TAG=\"20230901\"\ndocker build --platform linux/amd64 -t eeholmes/py-rocket-base:${DOCKER_TAG} -t eeholmes/py-rocket-base:main .\ndocker push eeholmes/py-rocket-base:${DOCKER_TAG}\ndocker push eeholmes/py-rocket-base:main\nTesting\nDOCKER_TAG=\"$(git rev-parse --short HEAD)\"\ndocker build --platform linux/amd64 -t eeholmes/py-rocket-base:${DOCKER_TAG} .\ndocker push eeholmes/py-rocket-base:${DOCKER_TAG}\nLog into Azure portal, go to DaskHub, Connect, Cloud Shell, and edit dconfig2.yaml (nano dconfig2.yaml) to update the image tag. Then run this command.\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\nTip: if things fill up use\ndocker system prune --all" - }, - { - "objectID": "posts/Set-up-centos-security.html", - "href": "posts/Set-up-centos-security.html", - "title": "Set-up CentOS https", - "section": "", - "text": "Now that our basic JupyterHub is running, we want to secure it. We are going to use Let’s Encrypt. Prerequisites:\nReferences:", - "crumbs": [ - "Centos Set-up https" - ] - }, - { - "objectID": "posts/Set-up-centos-security.html#create-a-domain-name", - "href": "posts/Set-up-centos-security.html#create-a-domain-name", - "title": "Set-up CentOS https", - "section": "Create a domain name", - "text": "Create a domain name\nFind a domain name provider and set one up. It is not expensive. I used GoDaddy. You only need one. Later you can use it for multiple hubs using subdomains where are created by the next step (DNS entry). For example, let’s say you get the domain bluemountain123.live. You can have as many subdomains as you want and they will be subdomain.bluemountain123.live.\n\nCreate a DNS entry\nLet’s pretend you set up bluemountain123.live as the domain. Go to the DNS settings for your domain. Add a type A record. This will do 2 things. First this will create the subdomain that you will use to access your JupyterHub. So let’s say you create, dhub as the type A DNS entry. Put dhub in the name and the public IP address of the server (leaving off :8000) in the value section. Then dhub.bluemountain123.live will be the url.\n\n\n\nTest if the url is working\nhttp://dhub.bluemountain123.live:8000 would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:", - "crumbs": [ - "Centos Set-up https" - ] - }, - { - "objectID": "posts/Set-up-centos-security.html#prep-the-server", - "href": "posts/Set-up-centos-security.html#prep-the-server", - "title": "Set-up CentOS https", - "section": "Prep the server", - "text": "Prep the server\n\nOpen port 80\nThis is the default port for http and certbot is going to spin up a temporary webserver on this port and get the SSL certificates. We will close this port when we are done.\n\nGo to the Azure dashboard (Networking section) for your CentOS server and make sure port 80 is open.\nCheck that the firewall is not blocking port 80: sudo firewall-cmd --list-ports. If 80 is not listed, we need to add it and reload:\n\n\nsudo firewall-cmd --permanent --add-port 80/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\n\n\nStop our JupyterHub\n\nsudo systemctl start jupyterhub.service", - "crumbs": [ - "Centos Set-up https" - ] - }, - { - "objectID": "posts/Set-up-centos-security.html#install-certbot", - "href": "posts/Set-up-centos-security.html#install-certbot", - "title": "Set-up CentOS https", - "section": "Install certbot", - "text": "Install certbot\nPer Let’s Encrypt recommendations, we will use certbot to get our SSL certificates. https://certbot.eff.org/.\nHere are the instructions for certbot on CentOS 8: https://certbot.eff.org/instructions?ws=other&os=centosrhel8 We choose “other” as the software.\n\nUpdate the CentOS repos\nI am using an End-of-Life CentOS distribution (sigh), and the repositories have been archived. This solution worked.\n\ndnf --disablerepo '*' --enablerepo=extras swap centos-linux-repos centos-stream-repos\ndnf distro-sync\n\nNote the last line, suggesting updating a bunch of packages and I said NO to that.\n\n\nInstall snap\nPer instructions here: https://snapcraft.io/docs/installing-snap-on-centos This updated some SELinux packages, which seemed a bit alarming but nothing seemed to break.\n\nsudo yum install snapd\nsudo systemctl enable --now snapd.socket\nsudo ln -s /var/lib/snapd/snap /snap\n\n\n\nInstall certbot\nI had to run this twice. First time it complained.\n\nsudo snap install --classic certbot\nsudo ln -s /snap/bin/certbot /usr/bin/certbot\n\n\n\nCreate the SSL certs.\nHave certbot create the SSL certs by spinning up a temporary webserver listening on port 80. Per instructions on the certbot website.\n\nsudo certbot certonly --standalone\n\nIt’ll ask for your email and the URL of your website. In my toy example, I created the domain dhub.bluemountain123.live.\n\n\nSSL cert renewal\nWith certbot running, the certificates should auto renew, but I haven’t tested this.", - "crumbs": [ - "Centos Set-up https" - ] - }, - { - "objectID": "posts/Set-up-centos-security.html#update-the-jupyterhub-config-file", - "href": "posts/Set-up-centos-security.html#update-the-jupyterhub-config-file", - "title": "Set-up CentOS https", - "section": "Update the JupyterHub config file", - "text": "Update the JupyterHub config file\nEdit with something like\n\ncd /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/\nnano jupyterhub_config.py\n\nThen add this to the config file. The port that is configured for SSL by default is 443. https is not going to work on 8000 which we had configured for http (on Azure).\nc.JupyterHub.port = 443\nc.JupyterHub.ssl_key = '/etc/letsencrypt/live/dhub.bluemountain123.live/privkey.pem'\nc.JupyterHub.ssl_cert = '/etc/letsencrypt/live/dhub.bluemountain123.live/fullchain.pem'\n\nRestart and Test\nWe need to open 443 in the firewall, and we can close 80 and 8000 now.\n\nsudo firewall-cmd --permanent --add-port 443/tcp\nsudo firewall-cmd --permanent --remove-port=80/tcp\nsudo firewall-cmd --permanent --remove-port=8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nNext we restart our JupyterHub service.\n\nsudo systemctl start jupyterhub.service\n\nTry https://dhub.bluemountain123.live and you should see the JupyterHub login without the http warning.", - "crumbs": [ - "Centos Set-up https" - ] - }, - { - "objectID": "posts/set-up-vm.html", - "href": "posts/set-up-vm.html", - "title": "Set up VM", - "section": "", - "text": "For testing JupyterHub set-ups, I start various Linux machines. Here is how to set up a virtual machines.", - "crumbs": [ - "Set-up VM on mac" - ] - }, - { - "objectID": "posts/set-up-vm.html#azure", - "href": "posts/set-up-vm.html#azure", - "title": "Set up VM", - "section": "Azure", - "text": "Azure\n\nCreated a Centos 8.3 server on Azure: https://portal.azure.com/#create/cloud-infrastructure-services.centos-8-3centos-8-3\nI didn’t do anything special for set-up. Choose SSH with key.\nOnce it is created, I went to the dashboard and selected my VM. The dashboard has a “Connect” button to get to the shell and it shows the public IP address.\nI had to create a special security rule to allow me to ssh into the public IP address to connect. Normally I use the cloud shell to connect, but Azure would not let me connect via the cloud shell for a server since it wanted upgraded security and I cannot do that with my work subscription.\nThen I saved the key somewhere on my computer and\n\nchmod 400 ~/<key location>\nssh -i ~/<key location> <vm-username>@<public key>\n\nI downloaded VMware Fusion 13.0.2 for M1 macs.\nThen I downloaded a Centos 9 server image from here\nhttps://www.centos.org/download/\nOpen VMWare and create a new VM. Choose other Linux. Doesn’t actually matter since it will be removed.\nShut down the VM.\nGo to settings and remove the hard drive.\nAdd a new hardrive. For me, I used ‘Add Device’ in the upper right of the Settings box. Choose ‘existing harddrive’\nHelp for M1 https://medium.com/@thehippieandtheboss/how-to-create-a-linux-virtual-machine-on-macos-1278ec1ef327\nhttps://tomcudd.com/how-i-set-up-a-centos-7-virtual-machine/", - "crumbs": [ - "Set-up VM on mac" - ] - }, - { - "objectID": "posts/Set-up-centos.html", - "href": "posts/Set-up-centos.html", - "title": "Centos Set-up", - "section": "", - "text": "This is my notes for setting this up on a Centos 8 (Linux distribution) server. Jump to the “Summary” section to see only the instructions without explanations.\nAll the commands are run in a shell (bash)\nReferences:", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#set-up-vm-on-azure", - "href": "posts/Set-up-centos.html#set-up-vm-on-azure", - "title": "Centos Set-up", - "section": "Set up VM on Azure", - "text": "Set up VM on Azure\n\nCreated a Centos 8.3 server on Azure: https://portal.azure.com/#create/cloud-infrastructure-services.centos-8-3centos-8-3\nI didn’t do anything special for set-up. Choose SSH with key.\nOnce it is created, I went to the dashboard and selected my VM. The dashboard has a “Connect” button to get to the shell and it shows the public IP address.\nI had to create a special security rule to allow me to ssh into the public IP address to connect. Normally I use the cloud shell to connect, but Azure would not let me connect via the cloud shell for a server since it wanted upgraded security package and I cannot do that with my work subscription.\nThen I saved the key somewhere on my computer and\n\n\nchmod 400 ~/<key location>\nssh -i ~/<key location>/Centos8.cer <vm-username>@<public ip-address>", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#on-vm-check-set-up", - "href": "posts/Set-up-centos.html#on-vm-check-set-up", - "title": "Centos Set-up", - "section": "On VM check set-up", - "text": "On VM check set-up\nI ssh-ed into the VM with\n\nssh -i <path to key downloaded from Azure> eeholmes@<public ip address>\n\n\nMake sure you are root\nGetting the JupyterHub set up needs to be done as root. First make sure you have an admin password. When I set up my Azure VM, I did not set a password. So first\n\nsudo passwd <your username>\n\nand set a password. Then switch to root if you are not signed in as root\n\nsudo -i\n\n\n\nCheck for Python\nYou will need Python 3.6+ installed. Open a terminal window and run python3 --version or python --version to see if Python is installed and what the version is.\nCheck your operating system (OS) with\n\ncat /etc/os-release\n\n\n\nCheck for conda\nYou will need conda (or miniconda) for these instructions. conda (and miniconda) take care of checking that all our packages will be inter-operable. It is best to install JupyterHub into a clean environment. That way you minimize chances of conflicts and your environment will solve (figure out any conflicts) much much faster.\nCheck for conda with\n\nconda list\n\nIf it doesn’t show a list of environments, then you need to install miniconda. Installation instructions. Read about miniconda for scientists from Software Carpentries here.\nThis is what I used to install miniconda from these instructions. Note install miniconda in some place like /opt/miniconda3 where all users will have access to `/opt/miniconda3/bin. We don’t want to install in /root/ for example or the admin users home directory.\n\nmkdir -p /opt/miniconda3\nwget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /opt/miniconda3/miniconda.sh\nbash /opt/miniconda3/miniconda.sh -b -u -p /opt/miniconda3\nrm -rf /opt/miniconda3/miniconda.sh\n\nThen initialize to set up the path. Note I am using bash. You’ll need to change if you are using zsh.\n\n/opt/miniconda3/bin/conda init bash\nsource ~/.bashrc\n\nnote will need to do something else to add the conda binary to all the users’ paths", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#create-the-conda-environment", - "href": "posts/Set-up-centos.html#create-the-conda-environment", - "title": "Centos Set-up", - "section": "Create the conda environment", - "text": "Create the conda environment\nCreate the conda environment for the jupyterhub installation. Installation will be in a directory with all the files for packages. Then activate it (enter it), and get the location of the environment (folder).\nAll the commands below are in the terminal window on your VM/server.\nCreate the environment named jupyterhub with python and jupyterhub (module). After creating, activate (enter) that environment. Then install jupyterlab, notebook and dockerspawner into the environment. Note the jupyterhub after -n is the name of the environment.\n\nconda create -n jupyterhub python\n\nThen activate (enter) that environment\n\nconda activate jupyterhub\n\nThen install jupyterhub here\n\nconda install -c conda-forge jupyterhub\n\nand then jupyterlab\n\nconda install -c conda-forge jupyterlab notebook\n\n\nSet a variable for env path\nThe environment has a folder with all the packages and binaries that we install. We are going to need to know the location of that folder. Get the location with\n\nconda env list\n\nOn the VM I set up, the folder location is\n\n/opt/miniconda3/envs/jupyterhub\n\nYours could be something entirely different. On another server with anaconda (a not-free conda package resolver), the folder was\n\n/SHARE/anaconda3/envs/jupterhub/\n\nWe are going to be saving the configuration files for our JupyterHub in this folder. Let’s save the path to a variable so we don’t have to keep entering the whole path.\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\n\nMake sure users can read and execute this folder. They need to in order to be able to spawn instances for the hub.\n\nchmod 755 $JHUBENV\n\nYou should now be able to start the hub, but you will not be able to access it yet because you need to open the 8000 port. Type\n\n$JHUBENV/bin/jupyterhub\n\nand check that it starts. Then use Cntl-C to stop the hub.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#create-a-user-on-the-vm", - "href": "posts/Set-up-centos.html#create-a-user-on-the-vm", - "title": "Centos Set-up", - "section": "Create a user on the VM", - "text": "Create a user on the VM\nBy default, any user on the server will be able to login. Let’s create a test user so that we are not logging into our hub with the root user password. We will be using “http” until we secure it so passwords are potentially exposed.\n\nuseradd jhub\n\nand give it a password when it asks.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#open-the-8000-port", - "href": "posts/Set-up-centos.html#open-the-8000-port", - "title": "Centos Set-up", - "section": "Open the 8000 port", - "text": "Open the 8000 port\nFirewallD was not running on my Azure Centos server, so I started it up to manage the ports.\n\nsudo systemctl enable firewalld\nsudo systemctl start firewalld\n\nFind out the Public IP address for the server you are on; it’s listed on the Azure overview and networking page for the VM in the Azure portal. Then open the 8000 port.\nFirst find out what ports are open through the firewall\n\nsudo firewall-cmd --list-ports\n\nAdd the 8000 port, reload and recheck that it appears.\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nBecause I am on an Azure VM, I also have to set up a networking rule to allow the 8000 port. By default, all public access to the server is blocked. Go to the Azure dashboard, select your VM, then select Networking under Settings, and then click Add Inbound Port rule. I am pretty sure you need to select “http” instead of “https”.\nOnce the port is open, you should be able to reach your JupyterHub at http://XXX.XX.XX.XX:8000 (replace the XX’s with the Public IP address).\nBackground\nThe JupyterhHub is running by default on http://localhost:8000. This means that if you start the hub on a machine that you are logged into, you should be able to open a browser on that machine, enter http://localhost:8000 and the hub login page will appear. There are a few reasons that might not work\n\nYou are ssh-ing into a server and don’t have a browser to open. The browser on the computer that you are ssh-ing from is the “localhost” in this case and you need the “localhost” to be the server.\nYou are logged directly into your server, but it doesn’t have a browser installed.\n\nHowever http://localhost:8000 is actually not very useful. We are trying to create a hub that others can log into from their browsers.\nSo you need to determine the Public IP address for the server you are on. This is the IP address that you could enter into a browser. If you enter http://XXX.XX.XX.XX (replace with actual IP), then you should see a page of some sort. This indicates that the server is working. If you are on an internal network, then you will only be able to load the address if you are also on that network. But for security reason, ports will not be open by default. You need to open the 8000 port so that http://XXX.XX.XX.XX:8000 will be found.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#log-in", - "href": "posts/Set-up-centos.html#log-in", - "title": "Centos Set-up", - "section": "Log in!", - "text": "Log in!\nAt this point, you should be able to login with the jhub test account.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#set-up-a-configuration-file", - "href": "posts/Set-up-centos.html#set-up-a-configuration-file", - "title": "Centos Set-up", - "section": "Set up a configuration file", - "text": "Set up a configuration file\nSo far, we have started the hub with the default configuration. We are going to need to customize it. For that we need a configuration file. We will create this in the folder where the environment files are.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\n\nNext create the default configuration file jupyterhub_config.py.\n\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nBecause we cd-d into the $JHUBENV/etc/jupyterhub/ directory, the file is created there. This default file is very long. Open up with\n\nnano jupyterhub_config.py\n\nUse F6 to find lines. Uncomment these two lines and save (Cntl-O, Enter, Cntl-X).\n\nc.Spawner.http_timeout = 3600", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#make-a-new-server-service", - "href": "posts/Set-up-centos.html#make-a-new-server-service", - "title": "Centos Set-up", - "section": "Make a new server service", - "text": "Make a new server service\n\nCreate the new unit file\nAt this point, after opening the port, you should be able to get to your JupyterHub by starting it with jupyterhub --ip XXX.XX.XX.XX --port=8000 and then browsing to http://XXX.XX.XX.XX:8000. But you hub is going to be stopped whenever the server is rebooted. So next we need to set up a service for your service so that our hub starts automatically.\nCreate a new directory for the service unit file,\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\n\nCreate the file and name jupyterhub.service. For example, using nano editor, we do\n\nnano jupyterhub.service\n\nAnd into that file we put the following. Replace /opt/miniconda3/envs/jupyterhub with the actual path to the jupyterhub environment folder.\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nNext we make systemd aware of the new service.\nCreate a symlink file in the folder where all the server services are kept. And tell systemd to reload its configuration files\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\n\n\n\nMake sure SELinux doesn’t block our service\nSELinux (security for the server) checks that files that are used have the correct label. All our files have generic file labels. If you do,\n\nls -Z $JHUBENV/etc/systemd/\n\nYou will see that the file label is unconfined_u:object_r:usr_t:s0. We need it to be\n\nsystemd_unit_file_t\n\nWe change the file label with\n\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\n\nSELinux will also object to the file label on all the binaries that we use to start up the JupyterHub (like jupyterhub) so we need to fix those file labels.\nThis will add bin_t label to all the binaries and check that it worked.\n\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\nls -Z $JHUBENV/bin\n\nIt got all the binaries but not the simlinks. Nonetheless it seemed to run ok.\n\n\nEnable our new service\n\nsudo systemctl enable jupyterhub.service\n\nThe service will start on reboot, but we can start it straight away using start:\n\nsudo systemctl start jupyterhub.service\n\nCheck that it is running.\n\nsudo systemctl status jupyterhub.service\n\nIf it fails, try\n\naudit2why < /var/log/audit/audit.log\n\nto debug. It is likely to be an issue with SELinux blocking the service from starting.\nNow our hub should be available on http:\\\\XXX.XX.XX.XX:8000. You can double check that it is listen on this port by running\n\nnetstat -tuln\n\nAt this point, you will need to address security if your hub is open to the web, as opposed to being on an internal network and only accessible to that network. Learn about that here.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#set-up-docker-for-user-environment", - "href": "posts/Set-up-centos.html#set-up-docker-for-user-environment", - "title": "Centos Set-up", - "section": "Set up Docker for user environment", - "text": "Set up Docker for user environment\nWhen you log in the jupyter notebooks will be trying to use the Python environment that was created to install JupyterHub, this is not what we want. We will use a docker image to “spawn” the user environment. Read here for other approaches.\nWe are going to use dockerspawner so that we can use a docker image for our user environments. The user will work in these containerized environments and they won’t have access to any other files in the server. In order to share their work with others, the normal workflow would be to work in Git repos and share those repos to a GitHub (or GitLab server). Each user will have a home directory on the server for their files, but they won’t have access to other hub user directories nor will they have access to any other directories on the server.\n\nInstall docker\nI am using Centos in this example\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nThen we need to start docker\n\nsudo systemctl start docker\n\n\n\nInstall dockerspawner\nI am going to be creating the user environment from a docker image, so I also want dockerspawner. Note dockerspawner installed docker-py but it was really old and threw errors so I installed separately to get the latest version. Note make sure you are in the jupyterhub conda env. You can run conda env list and use conda activate jupyterhub if you are not in it.\n\nconda install -c conda-forge dockerspawner\nconda install -c conda-forge docker-py\n\n\n\nJupyter images\nThe image that we use must have the jupyterhub and notebook module installed. The jupyterhub version needs to also match what you have on your hub.\nCheck the version on your server:\n\n$JHUBENV/bin/jupyterhub -V\n\nFor demo purposes, we will use the jupyter images on DockerHub. We want to find an image with the same version of jupyterhub as we have on our server.\n\n\nEdit the config file\nEdit the jupyterhub_config.py file in $JHUB-ENV/etc/jupyterhub/ to add that we want to use DockerSpawner and specify the images that users should have access to. Users will get a drop down menu. Add these lines to jupyterhub_config.py. The hub bind url needs to be 0.0.0.0 because we are using a docker container for the individual user environments.\n\nhttps://discourse.jupyter.org/t/whats-the-main-difference-between-hub-connect-url-vs-hub-bind-url/3596/2\nNote image_whitelist is deprecated as of dockerspawner 12.0. New name is allowed_images.\n\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nDo a docker pull of the images so that they don’t have to be pulled the first time that a user chooses that image.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nNow you can restart the service and the user can start a notebook with the specified images.\n\n\nCreate your own Docker images\nDocker images that work with JupyterHub with Kubernetes will work with this set-up with the addition of jupyterhub and notebook.\nAdd the following to your Docker image\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using rocker image. Code added to make the home directory home/jovyan.\n\nFROM rocker/binder:4.3\n\nUSER root\nRUN usermod -d /home/jovyan rstudio\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nWORKDIR /home/jovyan\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using openscapes/rocker\n\nFROM openscapes/rocker:a7596b5\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nUSER root\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nCMD [\"jupyterhub-singleuser\"]\n\n\n\nSpecial note regarding rocker images\nThe default home directory for rocker images is home/rstudio but the default for JupyterHub is home/jovyan.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#persistent-volume", - "href": "posts/Set-up-centos.html#persistent-volume", - "title": "Centos Set-up", - "section": "Persistent volume", - "text": "Persistent volume\nAdd the following to the config file to create a persistent volume.\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#user-environment-customization", - "href": "posts/Set-up-centos.html#user-environment-customization", - "title": "Centos Set-up", - "section": "User environment customization", - "text": "User environment customization\n\nMemory limits and guarantees\nYou can set memory limits on the containers that are spawned for users by adding limits. Read the documentation here.\nFor example:\n\nc.DockerSpawner.mem_limit = '8G'\nc.DockerSpawner.mem_guarantee = '1G'\nc.DockerSpawner.cpu_guarantee = 0.5\nc.DockerSpawner.cpu_limit = 1\n\nIf that doesn’t work try\n\nc.Spawner.mem_limit = '2G'\n\nI believe you can specify as a drop-down to give the user choices:\n\nc.DockerSpawner.mem_guarantee = {\n '1G': '1G',\n '2G': '2G',\n '8G': '8G',\n}\n\n\n\nCreating a shared volume\nOne read-only shared volume:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\n\nc.DockerSpawner.volumes = { 'jupyterhub-{username}':'/home/jovyan', '/path/to/shared': {\"bind\": '/home/jovyan/shared', \"mode\": \"ro\"} }\n\nA volume that is read-only for some and read-write for others:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\nMore discussions around shared volumes\nhttps://github.com/jupyterhub/dockerspawner/issues/453", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#setting-up-https", - "href": "posts/Set-up-centos.html#setting-up-https", - "title": "Centos Set-up", - "section": "Setting up https", - "text": "Setting up https\nIf you are using a public IP address, rather than being on a private network, you need to set up https so that content (passwords and everything else) is not visible. Read how to do that here.\nThese instructions set up this url: https://dhub.bluemountain123.live", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#github-authentication", - "href": "posts/Set-up-centos.html#github-authentication", - "title": "Centos Set-up", - "section": "GitHub authentication", - "text": "GitHub authentication\nAfter you have https set-up, we can set up authentication via a GitHub teams in a GitHub organization. Read other ways to authenticate (create users) here.\nhttps://oauthenticator.readthedocs.io/en/latest/tutorials/provider-specific-setup/providers/github.html\n\nCreate a new Oauth Application on GitHub\nThis Oauth application is going to be associated with your (personal) GitHub account, but you will use a team on a GitHub organization that you are owner of for the users who are allowed to log into your JupyterHub.\nLog into GitHub and go to GitHub > Settings > Developer Settings > New Oauth Application\nLook carefully at how I filled in the boxes. Change the URL and the name of the application.\n\nNext you will see something like this\n\nYou need to copy the ID and then click the create secrets button and save the secret. You will need those in the next step.\n\n\nCreate a team in your GitHub organization\nYou will be added by default and add anyone else who needs access to the hub. Let’s say your GitHub organization is MyOrg and the team is called JHub. So then the allowed organization is MyOrg:JHub. You can leave off :JHub if you want to allow all members of the organization to log in.\n\n\nInstall\nInstall the oauthenticator package. Make sure you are in the jupyterhub conda environment.\n\n# check what environment you are in and switch if needed\n# conda env list\n# conda activate jupyterhub\nconda install -c conda-forge oauthenticator\n\n\n\nEdit the jupyterhub_config.py file\nEdit with something like\n\ncd /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/\nnano jupyterhub_config.py\n\nAdd these info. Replace the id, secret, url and admin user with your values. Adding an admin user is handy because then you can do some basic management of the hub. Read more here.\nc.JupyterHub.authenticator_class = \"github\"\nc.OAuthenticator.oauth_callback_url = \"https://dhub.bluemountain123.live/hub/oauth_callback\"\nc.OAuthenticator.client_id = \"your oauth2 application id\"\nc.OAuthenticator.client_secret = \"your oauth2 application secret\"\nc.GitHubOAuthenticator.allowed_organizations = {\"MyOrg:JHub\"}\nc.GitHubOAuthenticator.scope = [\"read:org\"]\nc.GitHubOAuthenticator.admin_users = {\"eeholmes\"}\n\n\nRestart the hub\n\nsudo systemctl stop jupyterhub.service\nsudo systemctl start jupyterhub.service\n\nNow any member you add to the GitHub organization team should be able to log in.\nIf you run into trouble, try\n\nsudo systemctl status jupyterhub.service", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos.html#summary", - "href": "posts/Set-up-centos.html#summary", - "title": "Centos Set-up", - "section": "Summary", - "text": "Summary\nOnly the instructions. Make sure you are installing as the root user. I assume you have Python and conda installed.\nCreate the conda environment\n\nsudo -i\n\nconda create -n jupyterhub python --yes\nconda activate jupyterhub\nconda install -c conda-forge jupyterhub --yes\nconda install -c conda-forge jupyterlab notebook --yes\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\nchmod 755 $JHUBENV\n\nCreate user\n\nuseradd jhub\n\nOpen the 8000 port for access to the application.\n\n#sudo systemctl enable firewalld\n#sudo systemctl start firewalld\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nCreate the configuration file. Will be edited at end.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nInstall docker if needed\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nsudo systemctl start docker\n\nNot sure this is needed.\n\nsudo firewall-cmd --zone=docker --add-port=8081/tcp\nsudo firewall-cmd --reload\nsudo systemctl restart docker\n\nInstall dockerspawner\n\nconda install -c conda-forge dockerspawner --yes\nconda install -c conda-forge docker-py --yes\n\nEdit the configuration file.\n\ncd $JHUBENV/etc/jupyterhub/\nnano jupyterhub_config.py\n\nPaste this in\n\n# Configuration file for jupyterhub.\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'iorocker': 'eeholmes/iorocker-standalone:20231003',\n 'rocker-binder': 'eeholmes/rocker-binder:20231003',\n 'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }\n\nDocker pull of the images. Do all.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nMake a new server service\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\nnano jupyterhub.service\n\nPaste this in\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nMake sure SELinux doesn’t block our service\n\nls -Z $JHUBENV/etc/systemd/\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\n\nEnable our new service\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\nsudo systemctl enable jupyterhub.service\nsudo systemctl start jupyterhub.service\n\nDone! See the long instructions if anything is not working.\nNow go through the https and GitHub authentication steps if you need that.", - "crumbs": [ - "Centos Set-up" - ] - }, - { - "objectID": "posts/Set-up-centos-tljh.html", - "href": "posts/Set-up-centos-tljh.html", + "objectID": "Set-up-centos-tljh.html", + "href": "Set-up-centos-tljh.html", "title": "Centos Set-up with TLJH", "section": "", "text": "This is my notes for setting this up on a Centos 8 (Linux distribution) server. Jump to the “Summary” section to see only the instructions without explanations.\nAll the commands are run in a shell (bash)\nReferences:" }, { - "objectID": "posts/Set-up-centos-tljh.html#set-up-vm-on-azure", - "href": "posts/Set-up-centos-tljh.html#set-up-vm-on-azure", + "objectID": "Set-up-centos-tljh.html#set-up-vm-on-azure", + "href": "Set-up-centos-tljh.html#set-up-vm-on-azure", "title": "Centos Set-up with TLJH", "section": "Set up VM on Azure", "text": "Set up VM on Azure\n\nCreated a Centos 8.3 server on Azure: https://portal.azure.com/#create/cloud-infrastructure-services.centos-8-3centos-8-3\nI didn’t do anything special for set-up. Choose SSH with key.\nOnce it is created, I went to the dashboard and selected my VM. The dashboard has a “Connect” button to get to the shell and it shows the public IP address.\nI had to create a special security rule to allow me to ssh into the public IP address to connect. Normally I use the cloud shell to connect, but Azure would not let me connect via the cloud shell for a server since it wanted upgraded security package and I cannot do that with my work subscription.\nThen I saved the key somewhere on my computer and\n\n\nchmod 400 ~/<key location>\nssh -i ~/<key location>/Centos8.cer <vm-username>@<public ip-address>" }, { - "objectID": "posts/Set-up-centos-tljh.html#on-vm-check-set-up", - "href": "posts/Set-up-centos-tljh.html#on-vm-check-set-up", + "objectID": "Set-up-centos-tljh.html#on-vm-check-set-up", + "href": "Set-up-centos-tljh.html#on-vm-check-set-up", "title": "Centos Set-up with TLJH", "section": "On VM check set-up", "text": "On VM check set-up\nI ssh-ed into the VM with\n\nssh -i <path to key downloaded from Azure> eeholmes@<public ip address>\n\n\nMake sure you are root\nGetting the JupyterHub set up needs to be done as root. First make sure you have an admin password. When I set up my Azure VM, I did not set a password. So first\n\nsudo passwd <your username>\n\nand set a password. Then switch to root if you are not signed in as root\n\nsudo -i\n\n\n\nCheck for Python\nYou will need Python 3.6+ installed. Open a terminal window and run python3 --version or python --version to see if Python is installed and what the version is.\nCheck your operating system (OS) with\n\ncat /etc/os-release\n\n\n\nCheck for conda\nYou will need conda (or miniconda) for these instructions. conda (and miniconda) take care of checking that all our packages will be inter-operable. It is best to install JupyterHub into a clean environment. That way you minimize chances of conflicts and your environment will solve (figure out any conflicts) much much faster.\nCheck for conda with\n\nconda list\n\nIf it doesn’t show a list of environments, then you need to install miniconda. Installation instructions. Read about miniconda for scientists from Software Carpentries here.\nThis is what I used to install miniconda from these instructions. Note install miniconda in some place like /opt/miniconda3 where all users will have access to `/opt/miniconda3/bin. We don’t want to install in /root/ for example or the admin users home directory.\n\nmkdir -p /opt/miniconda3\nwget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /opt/miniconda3/miniconda.sh\nbash /opt/miniconda3/miniconda.sh -b -u -p /opt/miniconda3\nrm -rf /opt/miniconda3/miniconda.sh\n\nThen initialize to set up the path. Note I am using bash. You’ll need to change if you are using zsh.\n\n/opt/miniconda3/bin/conda init bash\nsource ~/.bashrc\n\nnote will need to do something else to add the conda binary to all the users’ paths" }, { - "objectID": "posts/Set-up-centos-tljh.html#create-the-conda-environment", - "href": "posts/Set-up-centos-tljh.html#create-the-conda-environment", + "objectID": "Set-up-centos-tljh.html#create-the-conda-environment", + "href": "Set-up-centos-tljh.html#create-the-conda-environment", "title": "Centos Set-up with TLJH", "section": "Create the conda environment", "text": "Create the conda environment\nCreate the conda environment for the jupyterhub installation. Installation will be in a directory with all the files for packages. Then activate it (enter it), and get the location of the environment (folder).\nAll the commands below are in the terminal window on your VM/server.\nCreate the environment named jupyterhub with python and jupyterhub (module). After creating, activate (enter) that environment. Then install jupyterlab, notebook and dockerspawner into the environment. Note the jupyterhub after -n is the name of the environment.\n\nconda create -n jupyterhub python\n\nThen activate (enter) that environment\n\nconda activate jupyterhub\n\nThen install jupyterhub here\n\nconda install -c conda-forge jupyterhub\n\nand then jupyterlab\n\nconda install -c conda-forge jupyterlab notebook\n\n\nSet a variable for env path\nThe environment has a folder with all the packages and binaries that we install. We are going to need to know the location of that folder. Get the location with\n\nconda env list\n\nOn the VM I set up, the folder location is\n\n/opt/miniconda3/envs/jupyterhub\n\nYours could be something entirely different. On another server with anaconda (a not-free conda package resolver), the folder was\n\n/SHARE/anaconda3/envs/jupterhub/\n\nWe are going to be saving the configuration files for our JupyterHub in this folder. Let’s save the path to a variable so we don’t have to keep entering the whole path.\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\n\nMake sure users can read and execute this folder. They need to in order to be able to spawn instances for the hub.\n\nchmod 755 $JHUBENV\n\nYou should now be able to start the hub, but you will not be able to access it yet because you need to open the 8000 port. Type\n\n$JHUBENV/bin/jupyterhub\n\nand check that it starts. Then use Cntl-C to stop the hub." }, { - "objectID": "posts/Set-up-centos-tljh.html#create-a-user-on-the-vm", - "href": "posts/Set-up-centos-tljh.html#create-a-user-on-the-vm", + "objectID": "Set-up-centos-tljh.html#create-a-user-on-the-vm", + "href": "Set-up-centos-tljh.html#create-a-user-on-the-vm", "title": "Centos Set-up with TLJH", "section": "Create a user on the VM", "text": "Create a user on the VM\nBy default, any user on the server will be able to login. Let’s create a test user so that we are not logging into our hub with the root user password. We will be using “http” until we secure it so passwords are potentially exposed.\n\nuseradd jhub\n\nand give it a password when it asks." }, { - "objectID": "posts/Set-up-centos-tljh.html#open-the-8000-port", - "href": "posts/Set-up-centos-tljh.html#open-the-8000-port", + "objectID": "Set-up-centos-tljh.html#open-the-8000-port", + "href": "Set-up-centos-tljh.html#open-the-8000-port", "title": "Centos Set-up with TLJH", "section": "Open the 8000 port", "text": "Open the 8000 port\nFirewallD was not running on my Azure Centos server, so I started it up to manage the ports.\n\nsudo systemctl enable firewalld\nsudo systemctl start firewalld\n\nFind out the Public IP address for the server you are on; it’s listed on the Azure overview and networking page for the VM in the Azure portal. Then open the 8000 port.\nFirst find out what ports are open through the firewall\n\nsudo firewall-cmd --list-ports\n\nAdd the 8000 port, reload and recheck that it appears.\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nBecause I am on an Azure VM, I also have to set up a networking rule to allow the 8000 port. By default, all public access to the server is blocked. Go to the Azure dashboard, select your VM, then select Networking under Settings, and then click Add Inbound Port rule. I am pretty sure you need to select “http” instead of “https”.\nOnce the port is open, you should be able to reach your JupyterHub at http://XXX.XX.XX.XX:8000 (replace the XX’s with the Public IP address).\nBackground\nThe JupyterhHub is running by default on http://localhost:8000. This means that if you start the hub on a machine that you are logged into, you should be able to open a browser on that machine, enter http://localhost:8000 and the hub login page will appear. There are a few reasons that might not work\n\nYou are ssh-ing into a server and don’t have a browser to open. The browser on the computer that you are ssh-ing from is the “localhost” in this case and you need the “localhost” to be the server.\nYou are logged directly into your server, but it doesn’t have a browser installed.\n\nHowever http://localhost:8000 is actually not very useful. We are trying to create a hub that others can log into from their browsers.\nSo you need to determine the Public IP address for the server you are on. This is the IP address that you could enter into a browser. If you enter http://XXX.XX.XX.XX (replace with actual IP), then you should see a page of some sort. This indicates that the server is working. If you are on an internal network, then you will only be able to load the address if you are also on that network. But for security reason, ports will not be open by default. You need to open the 8000 port so that http://XXX.XX.XX.XX:8000 will be found." }, { - "objectID": "posts/Set-up-centos-tljh.html#log-in", - "href": "posts/Set-up-centos-tljh.html#log-in", + "objectID": "Set-up-centos-tljh.html#log-in", + "href": "Set-up-centos-tljh.html#log-in", "title": "Centos Set-up with TLJH", "section": "Log in!", "text": "Log in!\nAt this point, you should be able to login with the jhub test account." }, { - "objectID": "posts/Set-up-centos-tljh.html#set-up-a-configuration-file", - "href": "posts/Set-up-centos-tljh.html#set-up-a-configuration-file", + "objectID": "Set-up-centos-tljh.html#set-up-a-configuration-file", + "href": "Set-up-centos-tljh.html#set-up-a-configuration-file", "title": "Centos Set-up with TLJH", "section": "Set up a configuration file", "text": "Set up a configuration file\nSo far, we have started the hub with the default configuration. We are going to need to customize it. For that we need a configuration file. We will create this in the folder where the environment files are.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\n\nNext create the default configuration file jupyterhub_config.py.\n\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nBecause we cd-d into the $JHUBENV/etc/jupyterhub/ directory, the file is created there. This default file is very long. Open up with\n\nnano jupyterhub_config.py\n\nUse F6 to find lines. Uncomment these two lines and save (Cntl-O, Enter, Cntl-X).\n\nc.Spawner.http_timeout = 3600" }, { - "objectID": "posts/Set-up-centos-tljh.html#make-a-new-server-service", - "href": "posts/Set-up-centos-tljh.html#make-a-new-server-service", + "objectID": "Set-up-centos-tljh.html#make-a-new-server-service", + "href": "Set-up-centos-tljh.html#make-a-new-server-service", "title": "Centos Set-up with TLJH", "section": "Make a new server service", "text": "Make a new server service\n\nCreate the new unit file\nAt this point, after opening the port, you should be able to get to your JupyterHub by starting it with jupyterhub --ip XXX.XX.XX.XX --port=8000 and then browsing to http://XXX.XX.XX.XX:8000. But you hub is going to be stopped whenever the server is rebooted. So next we need to set up a service for your service so that our hub starts automatically.\nCreate a new directory for the service unit file,\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\n\nCreate the file and name jupyterhub.service. For example, using nano editor, we do\n\nnano jupyterhub.service\n\nAnd into that file we put the following. Replace /opt/miniconda3/envs/jupyterhub with the actual path to the jupyterhub environment folder.\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nNext we make systemd aware of the new service.\nCreate a symlink file in the folder where all the server services are kept. And tell systemd to reload its configuration files\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\n\n\n\nMake sure SELinux doesn’t block our service\nSELinux (security for the server) checks that files that are used have the correct label. All our files have generic file labels. If you do,\n\nls -Z $JHUBENV/etc/systemd/\n\nYou will see that the file label is unconfined_u:object_r:usr_t:s0. We need it to be\n\nsystemd_unit_file_t\n\nWe change the file label with\n\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\n\nSELinux will also object to the file label on all the binaries that we use to start up the JupyterHub (like jupyterhub) so we need to fix those file labels.\nThis will add bin_t label to all the binaries and check that it worked.\n\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\nls -Z $JHUBENV/bin\n\nIt got all the binaries but not the simlinks. Nonetheless it seemed to run ok.\n\n\nEnable our new service\n\nsudo systemctl enable jupyterhub.service\n\nThe service will start on reboot, but we can start it straight away using start:\n\nsudo systemctl start jupyterhub.service\n\nCheck that it is running.\n\nsudo systemctl status jupyterhub.service\n\nIf it fails, try\n\naudit2why < /var/log/audit/audit.log\n\nto debug. It is likely to be an issue with SELinux blocking the service from starting.\nNow our hub should be available on http:\\\\XXX.XX.XX.XX:8000. You can double check that it is listen on this port by running\n\nnetstat -tuln\n\nAt this point, you will need to address security if your hub is open to the web, as opposed to being on an internal network and only accessible to that network. Learn about that here." }, { - "objectID": "posts/Set-up-centos-tljh.html#set-up-docker-for-user-environment", - "href": "posts/Set-up-centos-tljh.html#set-up-docker-for-user-environment", + "objectID": "Set-up-centos-tljh.html#set-up-docker-for-user-environment", + "href": "Set-up-centos-tljh.html#set-up-docker-for-user-environment", "title": "Centos Set-up with TLJH", "section": "Set up Docker for user environment", "text": "Set up Docker for user environment\nWhen you log in the jupyter notebooks will be trying to use the Python environment that was created to install JupyterHub, this is not what we want. We will use a docker image to “spawn” the user environment. Read here for other approaches.\nWe are going to use dockerspawner so that we can use a docker image for our user environments. The user will work in these containerized environments and they won’t have access to any other files in the server. In order to share their work with others, the normal workflow would be to work in Git repos and share those repos to a GitHub (or GitLab server). Each user will have a home directory on the server for their files, but they won’t have access to other hub user directories nor will they have access to any other directories on the server.\n\nInstall docker\nI am using Centos in this example\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nThen we need to start docker\n\nsudo systemctl start docker\n\n\n\nInstall dockerspawner\nI am going to be creating the user environment from a docker image, so I also want dockerspawner. Note dockerspawner installed docker-py but it was really old and threw errors so I installed separately to get the latest version. Note make sure you are in the jupyterhub conda env. You can run conda env list and use conda activate jupyterhub if you are not in it.\n\nconda install -c conda-forge dockerspawner\nconda install -c conda-forge docker-py\n\n\n\nJupyter images\nThe image that we use must have the jupyterhub and notebook module installed. The jupyterhub version needs to also match what you have on your hub.\nCheck the version on your server:\n\n$JHUBENV/bin/jupyterhub -V\n\nFor demo purposes, we will use the jupyter images on DockerHub. We want to find an image with the same version of jupyterhub as we have on our server.\n\n\nEdit the config file\nEdit the jupyterhub_config.py file in $JHUB-ENV/etc/jupyterhub/ to add that we want to use DockerSpawner and specify the images that users should have access to. Users will get a drop down menu. Add these lines to jupyterhub_config.py. The hub bind url needs to be 0.0.0.0 because we are using a docker container for the individual user environments.\n\nhttps://discourse.jupyter.org/t/whats-the-main-difference-between-hub-connect-url-vs-hub-bind-url/3596/2\nNote image_whitelist is deprecated as of dockerspawner 12.0. New name is allowed_images.\n\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nDo a docker pull of the images so that they don’t have to be pulled the first time that a user chooses that image.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nNow you can restart the service and the user can start a notebook with the specified images.\n\n\nCreate your own Docker images\nDocker images that work with JupyterHub with Kubernetes will work with this set-up with the addition of jupyterhub and notebook.\nAdd the following to your Docker image\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using rocker image. Code added to make the home directory home/jovyan.\n\nFROM rocker/binder:4.3\n\nUSER root\nRUN usermod -d /home/jovyan rstudio\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nWORKDIR /home/jovyan\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using openscapes/rocker\n\nFROM openscapes/rocker:a7596b5\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nUSER root\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nCMD [\"jupyterhub-singleuser\"]\n\n\n\nSpecial note regarding rocker images\nThe default home directory for rocker images is home/rstudio but the default for JupyterHub is home/jovyan." }, { - "objectID": "posts/Set-up-centos-tljh.html#persistent-volume", - "href": "posts/Set-up-centos-tljh.html#persistent-volume", + "objectID": "Set-up-centos-tljh.html#persistent-volume", + "href": "Set-up-centos-tljh.html#persistent-volume", "title": "Centos Set-up with TLJH", "section": "Persistent volume", "text": "Persistent volume\nAdd the following to the config file to create a persistent volume.\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }" }, { - "objectID": "posts/Set-up-centos-tljh.html#user-environment-customization", - "href": "posts/Set-up-centos-tljh.html#user-environment-customization", + "objectID": "Set-up-centos-tljh.html#user-environment-customization", + "href": "Set-up-centos-tljh.html#user-environment-customization", "title": "Centos Set-up with TLJH", "section": "User environment customization", "text": "User environment customization\n\nMemory limits and guarantees\nYou can set memory limits on the containers that are spawned for users by adding limits. Read the documentation here.\nFor example:\n\nc.DockerSpawner.mem_limit = '8G'\nc.DockerSpawner.mem_guarantee = '1G'\nc.DockerSpawner.cpu_guarantee = 0.5\nc.DockerSpawner.cpu_limit = 1\n\nIf that doesn’t work try\n\nc.Spawner.mem_limit = '2G'\n\nI believe you can specify as a drop-down to give the user choices:\n\nc.DockerSpawner.mem_guarantee = {\n '1G': '1G',\n '2G': '2G',\n '8G': '8G',\n}\n\n\n\nCreating a shared volume\nOne read-only shared volume:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\n\nc.DockerSpawner.volumes = { 'jupyterhub-user-{username}':'/home/jovyan', '/path/to/shared': {\"bind\": '/home/jovyan/shared', \"mode\": \"ro\"} }\n\nA volume that is read-only for some and read-write for others:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\nMore discussions around shared volumes\nhttps://github.com/jupyterhub/dockerspawner/issues/453" }, { - "objectID": "posts/Set-up-centos-tljh.html#setting-up-https", - "href": "posts/Set-up-centos-tljh.html#setting-up-https", + "objectID": "Set-up-centos-tljh.html#setting-up-https", + "href": "Set-up-centos-tljh.html#setting-up-https", "title": "Centos Set-up with TLJH", "section": "Setting up https", "text": "Setting up https\nIf you are using a public IP address, rather than being on a private network, you will want to set up https.\n\nCreate a domain name\nFind a domain name provider and set one up. It is not expensive. I used GoDaddy. You only need one. Later you can use it for multiple hubs using subdomains where are created by the next step (DNS entry). For example, let’s say you get the domain bluemountain123.live. You can have as many subdomains as you want and they will be subdomain.bluemountain123.live.\n\n\nCreate a DNS entry\nLet’s pretend you set up bluemountain123.live as the domain. Go to the DNS settings for your domain. Add a type A record. This will do 2 things. First this will create the subdomain that you will use to access your JupyterHub. So let’s say you create, dhub as the type A DNS entry. Put dhub in the name and the public IP address of the server (leaving off :8000) in the value section. Then dhub.bluemountain123.live will be the url.\n\n\n\nTest if the url is working\nhttp:\\\\dhub.bluemountain123.live:8000 would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:\n\n\n\nSet-up https on your JupyterHub\nLog back into your Kubernetes cluster: go to portal.azure.com, click on your Kubernetes cluster name, and then click on “Connect”. Then click on “Open Cloud Shell”. Read documentation about https\nOnce you are on the shell, type\nnano dconfig.yaml\nto edit the config file. Paste this in and save. Note the additional jupyterhub: in the yaml file. This is not in a plain JupyterHub with Kubernetes config file (i.e. in a non-daskhub, the jupyterhub: bit is not there and everything is moved to left by 2 spaces).\njupyterhub:\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com\n\n\nUpdate the JupyterHub installation\nAnytime you change dconfig.yaml you need to run this code.\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml\n\n\nTest if https is working\nTry https:\\\\dhub.bluemountain123.live and you should see the JupyterHub login without that http warning." }, { - "objectID": "posts/Set-up-centos-tljh.html#authentication", - "href": "posts/Set-up-centos-tljh.html#authentication", + "objectID": "Set-up-centos-tljh.html#authentication", + "href": "Set-up-centos-tljh.html#authentication", "title": "Centos Set-up with TLJH", "section": "Authentication", "text": "Authentication\nhttps://oauthenticator.readthedocs.io/en/latest/tutorials/provider-specific-setup/providers/github.html" }, { - "objectID": "posts/Set-up-centos-tljh.html#summary", - "href": "posts/Set-up-centos-tljh.html#summary", + "objectID": "Set-up-centos-tljh.html#summary", + "href": "Set-up-centos-tljh.html#summary", "title": "Centos Set-up with TLJH", "section": "Summary", "text": "Summary\nOnly the instructions. Make sure you are installing as the root user. I assume you have Python and conda installed.\nCreate the conda environment\n\nsudo -i\n\nconda create -n jupyterhub python --yes\nconda activate jupyterhub\nconda install -c conda-forge jupyterhub --yes\nconda install -c conda-forge jupyterlab notebook --yes\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\nchmod 755 $JHUBENV\n\nCreate user\n\nuseradd jhub\n\nOpen the 8000 port for access to the application.\n\n#sudo systemctl enable firewalld\n#sudo systemctl start firewalld\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nCreate the configuration file. Will be editted at end.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nInstall docker if needed\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nsudo systemctl start docker\n\nNot sure this is needed.\n\nsudo firewall-cmd --zone=docker --add-port=8081/tcp\nsudo firewall-cmd --reload\nsudo systemctl restart docker\n\nInstall dockerspawner\n\nconda install -c conda-forge dockerspawner --yes\nconda install -c conda-forge docker-py --yes\n\nEdit the configuration file.\n\ncd $JHUBENV/etc/jupyterhub/\nnano jupyterhub_config.py\n\nPaste this in\n\n# Configuration file for jupyterhub.\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'iorocker': 'eeholmes/iorocker-standalone:20231003',\n 'rocker-binder': 'eeholmes/rocker-binder:20231003',\n 'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }\n\nDocker pull of the images. Do all.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nMake a new server service\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\nnano jupyterhub.service\n\nPaste this in\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nMake sure SELinux doesn’t block our service\n\nls -Z $JHUBENV/etc/systemd/\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\n\nEnable our new service\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\nsudo systemctl enable jupyterhub.service\nsudo systemctl start jupyterhub.service\n\nDone! See the long instructions if anything is not working." }, { - "objectID": "posts/Set-up-daskhub.html", - "href": "posts/Set-up-daskhub.html", + "objectID": "Set-up-daskhub.html", + "href": "Set-up-daskhub.html", "title": "DaskHub Set-up", "section": "", - "text": "This is my notes for setting this up on Azure. Attempting to replicate the Openscapes 2i2c JupyterHub: https://github.com/2i2c-org/infrastructure/tree/master/config/clusters/openscapes\nThat hub is on AWS and is designed for large workshops (100+) however the NMFS OpenSci JHub is quite similar. Main difference at the moment is that I don’t have a shared drive set-up and the user persistent volume (storage) is on the same VM as the user node for their Jupyter Notebook. This means that I cannot have multiple VM sizes. Need to fix so that user can pick a larger VM for a task if needed.", - "crumbs": [ - "JHub Set-up" - ] + "text": "This is my notes for setting this up on Azure. Attempting to replicate the Openscapes 2i2c JupyterHub: https://github.com/2i2c-org/infrastructure/tree/master/config/clusters/openscapes\nThat hub is on AWS and is designed for large workshops (100+) however the NMFS OpenSci JHub is quite similar. Main difference at the moment is that I don’t have a shared drive set-up and the user persistent volume (storage) is on the same VM as the user node for their Jupyter Notebook. This means that I cannot have multiple VM sizes. Need to fix so that user can pick a larger VM for a task if needed." }, { - "objectID": "posts/Set-up-daskhub.html#create-your-kubernetes-cluster", - "href": "posts/Set-up-daskhub.html#create-your-kubernetes-cluster", + "objectID": "Set-up-daskhub.html#create-your-kubernetes-cluster", + "href": "Set-up-daskhub.html#create-your-kubernetes-cluster", "title": "DaskHub Set-up", "section": "Create your Kubernetes cluster", - "text": "Create your Kubernetes cluster\nLog into https:\\\\portal.azure.com\n\nGet to the dashboard that looks similar to this.\n\n\n\nClick on the Kubernetes Services button and you should see something like this\n\n\n\nClick Create Kubernetes Cluster\n\nAt this point, you will get to the set-up with lots of tabs.\n\nYou need to select the resource group if you are in a subscription for an organization. Don’t know what resource group to use, ask the admins.\nYou need to give your Kubernetes cluster a name. For example, jhub or daskhub or whatever.\nYou need to chose the AWS region. If you are using AWS S3 file access (big data in the cloud), then you need to be on the same region as the files you are accessing. Do you have no idea? Then you are probably not using AWS S3 file access. In that case, just go with the default or something close to you.\nNext you chose the “Node size”. This is the size of the base virtural machine (VM). It is going to spin up as many as it needs. The default is Standard DS2 v2 which as 2 CPU, 7 Gig RAM and 1T memory. This is fine for set-up. You can add more (bigger VMs later). Accept autoscaling since this is a multi-user hub.\n\nThe first tab is all you need for now. Later you may want to allow the user, to choose a different base VM. You can do that by adding node-pools. That’ll be covered after the initial set-up. For now, just get your basic hub working. You can add more VM sizes later.\n\nClick “Review and Create”\n\nWait for validation tests to pass.\n\nClick “Create”.\n\nOnce it is done deploying, you will see this.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Create your Kubernetes cluster\nLog into https:\\\\portal.azure.com\n\nGet to the dashboard that looks similar to this.\n\n\n\nClick on the Kubernetes Services button and you should see something like this\n\n\n\nClick Create Kubernetes Cluster\n\nAt this point, you will get to the set-up with lots of tabs.\n\nYou need to select the resource group if you are in a subscription for an organization. Don’t know what resource group to use, ask the admins.\nYou need to give your Kubernetes cluster a name. For example, jhub or daskhub or whatever.\nYou need to chose the AWS region. If you are using AWS S3 file access (big data in the cloud), then you need to be on the same region as the files you are accessing. Do you have no idea? Then you are probably not using AWS S3 file access. In that case, just go with the default or something close to you.\nNext you chose the “Node size”. This is the size of the base virtural machine (VM). It is going to spin up as many as it needs. The default is Standard DS2 v2 which as 2 CPU, 7 Gig RAM and 1T memory. This is fine for set-up. You can add more (bigger VMs later). Accept autoscaling since this is a multi-user hub.\n\nThe first tab is all you need for now. Later you may want to allow the user, to choose a different base VM. You can do that by adding node-pools. That’ll be covered after the initial set-up. For now, just get your basic hub working. You can add more VM sizes later.\n\nClick “Review and Create”\n\nWait for validation tests to pass.\n\nClick “Create”.\n\nOnce it is done deploying, you will see this." }, { - "objectID": "posts/Set-up-daskhub.html#install-daskhub-on-your-cluster", - "href": "posts/Set-up-daskhub.html#install-daskhub-on-your-cluster", + "objectID": "Set-up-daskhub.html#install-daskhub-on-your-cluster", + "href": "Set-up-daskhub.html#install-daskhub-on-your-cluster", "title": "DaskHub Set-up", "section": "Install DaskHub on your cluster", - "text": "Install DaskHub on your cluster\nThese next steps are done in the shell after connecting to your cluster. First you need to get to the shell.\n\nConnect to your cluster\nOnce you have created your Kubernetes cluster, you want to go to its dashboard (by clicking on the name you gave it). You’ll see something like this (I named mine daskhub).\n\nClick on the Connect icon in the nav bar at top.\nYou then see this\n\nClick on the link that says “Open Cloud Shell”.\n\nYou will get to a terminal. Paste in the two commands in the previous image (the commands that show up for you that is).\n\n\nCreate dconfig.yaml\nThis will be the configuration file for your Dask-enabled JupyterHub. For now, it can be just comments. Note the name is unimportant but should end in .yaml. I am using dconfig.yaml instead of config.yaml since I already have a config.yaml file for something else–and I have not figured out how to install different hubs in different directories or even different clusters in different directories (I have much to learn…).\nnano dconfig.yaml\nThis will open the nano editor. Edit your file. You can do # just blank for now. Then Cntl-O to save and Cntl-X to exit.\n\n\nInstall daskhub via helm chart\nInstructions: https://artifacthub.io/packages/helm/dask/daskhub .\nCheck that helm is installed\nhelm version\nTell helm about the dask helm repository\nhelm repo add dask https://helm.dask.org\nhelm repo update\nNow install\nhelm upgrade --wait --install --render-subchart-notes \\\n dhub dask/daskhub \\\n --namespace=dhub --create-namespace \\\n --values=dconfig.yaml\nYou will see this on successful installation (it’s long. much has been cut). \n\n\nSet-up your external IP address\nkubectl config set-context $(kubectl config current-context) --namespace dhub\nkubectl --namespace=dhub get service proxy-public\nThese commands will show the the IP address. Save the public IP address. You will need it in step 2. Look for the IP address under EXTERNAL-IP.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Install DaskHub on your cluster\nThese next steps are done in the shell after connecting to your cluster. First you need to get to the shell.\n\nConnect to your cluster\nOnce you have created your Kubernetes cluster, you want to go to its dashboard (by clicking on the name you gave it). You’ll see something like this (I named mine daskhub).\n\nClick on the Connect icon in the nav bar at top.\nYou then see this\n\nClick on the link that says “Open Cloud Shell”.\n\nYou will get to a terminal. Paste in the two commands in the previous image (the commands that show up for you that is).\n\n\nCreate dconfig.yaml\nThis will be the configuration file for your Dask-enabled JupyterHub. For now, it can be just comments. Note the name is unimportant but should end in .yaml. I am using dconfig.yaml instead of config.yaml since I already have a config.yaml file for something else–and I have not figured out how to install different hubs in different directories or even different clusters in different directories (I have much to learn…).\nnano dconfig.yaml\nThis will open the nano editor. Edit your file. You can do # just blank for now. Then Cntl-O to save and Cntl-X to exit.\n\n\nInstall daskhub via helm chart\nInstructions: https://artifacthub.io/packages/helm/dask/daskhub .\nCheck that helm is installed\nhelm version\nTell helm about the dask helm repository\nhelm repo add dask https://helm.dask.org\nhelm repo update\nNow install\nhelm upgrade --wait --install --render-subchart-notes \\\n dhub dask/daskhub \\\n --namespace=dhub --create-namespace \\\n --values=dconfig.yaml\nYou will see this on successful installation (it’s long. much has been cut). \n\n\nSet-up your external IP address\nkubectl config set-context $(kubectl config current-context) --namespace dhub\nkubectl --namespace=dhub get service proxy-public\nThese commands will show the the IP address. Save the public IP address. You will need it in step 2. Look for the IP address under EXTERNAL-IP." }, { - "objectID": "posts/Set-up-daskhub.html#step-2-set-up-https", - "href": "posts/Set-up-daskhub.html#step-2-set-up-https", + "objectID": "Set-up-daskhub.html#step-2-set-up-https", + "href": "Set-up-daskhub.html#step-2-set-up-https", "title": "DaskHub Set-up", "section": "Step 2 Set up https", - "text": "Step 2 Set up https\nYou can log out of your cluster. The next steps are done elsewhere.\n\nCreate a domain name\nYou will need a domain name for https which you want for security (and JHub won’t stop complaining if you don’t). Find a domain name provider and set one up. It is not expensive. I used GoDaddy.\n\n\nCreate a DNS entry\nLet’s pretend you set up bluemountain123.live as the domain. Go to the DNS settings for your domain. Add a type A record. This will do 2 things. First this will create the subdomain that you will use to access your JupyterHub. So let’s say you create, dhub as the type A DNS entry. Then dhub.bluemountain123.live will be the url. You can have as many subdomains as you need.\n\n\n\nTest if the url is working\nhttp:\\\\dhub.bluemountain123.live would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:\n\n\n\nSet-up https on your JupyterHub\nLog back into your Kubernetes cluster: go to portal.azure.com, click on your Kubernetes cluster name, and then click on “Connect”. Then click on “Open Cloud Shell”. Read documentation about https\nOnce you are on the shell, type\nnano dconfig.yaml\nto edit the config file. Paste this in and save. Note the additional jupyterhub: in the yaml file. This is not in a plain JupyterHub with Kubernetes config file (i.e. in a non-daskhub, the jupyterhub: bit is not there and everything is moved to left by 2 spaces).\njupyterhub:\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com\n\n\nUpdate the JupyterHub installation\nAnytime you change dconfig.yaml you need to run this code.\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml\n\n\nTest if https is working\nTry https:\\\\dhub.bluemountain123.live and you should see the JupyterHub login without that http warning. This might take 30 minutes to an hour.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Step 2 Set up https\nYou can log out of your cluster. The next steps are done elsewhere.\n\nCreate a domain name\nYou will need a domain name for https which you want for security (and JHub won’t stop complaining if you don’t). Find a domain name provider and set one up. It is not expensive. I used GoDaddy.\n\n\nCreate a DNS entry\nLet’s pretend you set up bluemountain123.live as the domain. Go to the DNS settings for your domain. Add a type A record. This will do 2 things. First this will create the subdomain that you will use to access your JupyterHub. So let’s say you create, dhub as the type A DNS entry. Then dhub.bluemountain123.live will be the url. You can have as many subdomains as you need.\n\n\n\nTest if the url is working\nhttp:\\\\dhub.bluemountain123.live would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:\n\n\n\nSet-up https on your JupyterHub\nLog back into your Kubernetes cluster: go to portal.azure.com, click on your Kubernetes cluster name, and then click on “Connect”. Then click on “Open Cloud Shell”. Read documentation about https\nOnce you are on the shell, type\nnano dconfig.yaml\nto edit the config file. Paste this in and save. Note the additional jupyterhub: in the yaml file. This is not in a plain JupyterHub with Kubernetes config file (i.e. in a non-daskhub, the jupyterhub: bit is not there and everything is moved to left by 2 spaces).\njupyterhub:\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com\n\n\nUpdate the JupyterHub installation\nAnytime you change dconfig.yaml you need to run this code.\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml\n\n\nTest if https is working\nTry https:\\\\dhub.bluemountain123.live and you should see the JupyterHub login without that http warning." }, { - "objectID": "posts/Set-up-daskhub.html#step-3-set-up-github-authentication", - "href": "posts/Set-up-daskhub.html#step-3-set-up-github-authentication", + "objectID": "Set-up-daskhub.html#step-3-set-up-github-authentication", + "href": "Set-up-daskhub.html#step-3-set-up-github-authentication", "title": "DaskHub Set-up", "section": "Step 3 Set up GitHub authentication", - "text": "Step 3 Set up GitHub authentication\nOptional, if you want to manage who can login via GitHub Team. I am going to show an example where I use a team on a GitHub organization to manage authentication. There are many other ways to manage users. Google to find that.\n\nCreate a new Oauth Application on GitHub\nThis is going to be associated with your (personal) GitHub account, but you can use a team on a GitHub org that you are owner of.\nLog into GitHub and go to GitHub > Settings > Developer Settings > New Oauth Application\nLook carefully at how I filled in the boxes.\n\nNext you will see something like this\n\nYou need to copy the ID and then click the create secrets button and save the secret. Save those for later.\n\n\nCreate a team in your GitHub org\nYou will be added by default and add anyone else who needs access to the hub. Let’s say your org is MyOrg and the team is called DaskHub. So then the allowed organization is MyOrg:DaskHub. You can leave off :DaskHub if you want to allow all members of the organization to log in.\n\n\nEdit the dconfig.yaml file\nnano dconfig.yaml\nAdd to your config file so it is now this. Replace the id, secret and url with your values. We need to set the KubeSpawner working directory because the Openscapes Docker image sets it to home/jovyan/.kernels–which is fine but annoying since .kernels is hidden and not $HOME.\njupyterhub:\n hub:\n config:\n GitHubOAuthenticator:\n client_id: <replace with your OAuth id>\n client_secret: <replace with your OAuth app secret>\n oauth_callback_url: https://dhub.bluemountain123.live/hub/oauth_callback\n allowed_organizations:\n - MyOrg:DaskHub\n scope:\n - read:org\n JupyterHub:\n authenticator_class: github\n KubeSpawner:\n working_dir: /home/jovyan\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com \n\n\nUpdate the hub\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml\n\n\nTest\nYou should now see this and can authenticate with GitHub.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Step 3 Set up GitHub authentication\nOptional, if you want to manage who can login via GitHub Team. I am going to show an example where I use a team on a GitHub organization to manage authentication. There are many other ways to manage users. Google to find that.\n\nCreate a new Oauth Application on GitHub\nThis is going to be associated with your (personal) GitHub account, but you can use a team on a GitHub org that you are owner of.\nLog into GitHub and go to GitHub > Settings > Developer Settings > New Oauth Application\nLook carefully at how I filled in the boxes.\n\nNext you will see something like this\n\nYou need to copy the ID and then click the create secrets button and save the secret. Save those for later.\n\n\nCreate a team in your GitHub org\nYou will be added by default and add anyone else who needs access to the hub. Let’s say your org is MyOrg and the team is called DaskHub. So then the allowed organization is MyOrg:DaskHub. You can leave off :DaskHub if you want to allow all members of the organization to log in.\n\n\nEdit the dconfig.yaml file\nnano dconfig.yaml\nAdd to your config file so it is now this. Replace the id, secret and url with your values. We need to set the KubeSpawner working directory because the Openscapes Docker image sets it to home/jovyan/.kernels–which is fine but annoying since .kernels is hidden and not $HOME.\njupyterhub:\n hub:\n config:\n GitHubOAuthenticator:\n client_id: <replace with your OAuth id>\n client_secret: <replace with your OAuth app secret>\n oauth_callback_url: https://dhub.bluemountain123.live/hub/oauth_callback\n allowed_organizations:\n - MyOrg:DaskHub\n scope:\n - read:org\n JupyterHub:\n authenticator_class: github\n KubeSpawner:\n working_dir: /home/jovyan\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com \n\n\nUpdate the hub\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml\n\n\nTest\nYou should now see this and can authenticate with GitHub." }, { - "objectID": "posts/Set-up-daskhub.html#set-up-the-container-image", - "href": "posts/Set-up-daskhub.html#set-up-the-container-image", + "objectID": "Set-up-daskhub.html#set-up-the-container-image", + "href": "Set-up-daskhub.html#set-up-the-container-image", "title": "DaskHub Set-up", "section": "Set up the container image", - "text": "Set up the container image\nNow you need to specify the Docker image that will be used. We will use 2 different profiles: Python and R (RStudio).\nEdit the dconfig.yaml file and add the user image info. Note the spacing matters (a lot). I also added some Dask gateway config.\njupyterhub:\n hub:\n config:\n GitHubOAuthenticator:\n client_id: <replace with your OAuth id>\n client_secret: <replace with your OAuth app secret>\n oauth_callback_url: https://dhub.bluemountain123.live/hub/oauth_callback\n allowed_organizations:\n - MyOrg:DaskHub\n scope:\n - read:org\n JupyterHub:\n authenticator_class: github\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com \n singleuser:\n image:\n name: openscapes/python\n tag: f577786\n cmd: null\n singleuser:\n # Defines the default image\n image:\n name: openscapes/python\n tag: f577786\n profileList:\n - display_name: \"Python3\"\n description: \"NASA Openscapes Python image\"\n default: true\n - display_name: \"R\"\n description: \"NASA Openscapes RStudio image\"\n kubespawner_override:\n image: openscapes/rocker:a7596b5 \ndask-gateway:\n gateway:\n extraConfig:\n idle: |-\n # timeout after 30 minutes of inactivity\n c.KubeClusterConfig.idle_timeout = 1800 \n\nUpdate the hub\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml", - "crumbs": [ - "JHub Set-up" - ] + "text": "Set up the container image\nNow you need to specify the Docker image that will be used. We will use 2 different profiles: Python and R (RStudio).\nEdit the dconfig.yaml file and add the user image info. Note the spacing matters (a lot). I also added some Dask gateway config.\njupyterhub:\n hub:\n config:\n GitHubOAuthenticator:\n client_id: <replace with your OAuth id>\n client_secret: <replace with your OAuth app secret>\n oauth_callback_url: https://dhub.bluemountain123.live/hub/oauth_callback\n allowed_organizations:\n - MyOrg:DaskHub\n scope:\n - read:org\n JupyterHub:\n authenticator_class: github\n proxy:\n https:\n enabled: true\n hosts:\n - dhub.bluemountain123.live\n letsencrypt:\n contactEmail: your@email.com \n singleuser:\n image:\n name: openscapes/python\n tag: f577786\n cmd: null\n singleuser:\n # Defines the default image\n image:\n name: openscapes/python\n tag: f577786\n profileList:\n - display_name: \"Python3\"\n description: \"NASA Openscapes Python image\"\n default: true\n - display_name: \"R\"\n description: \"NASA Openscapes RStudio image\"\n kubespawner_override:\n image: openscapes/rocker:a7596b5 \ndask-gateway:\n gateway:\n extraConfig:\n idle: |-\n # timeout after 30 minutes of inactivity\n c.KubeClusterConfig.idle_timeout = 1800 \n\nUpdate the hub\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig.yaml" }, { - "objectID": "posts/Set-up-daskhub.html#changing-the-vm-size", - "href": "posts/Set-up-daskhub.html#changing-the-vm-size", + "objectID": "Set-up-daskhub.html#changing-the-vm-size", + "href": "Set-up-daskhub.html#changing-the-vm-size", "title": "DaskHub Set-up", "section": "Changing the VM size", - "text": "Changing the VM size\nNOT WORKING YET I am stuck on creating the persistent volumes. Needed because you need the user storage somewhere if you have multiple node pools.\n\nkubectl get nodes --show-labels | grep instance-type\nbeta.kubernetes.io/instance-type=Standard_D8s_v3", - "crumbs": [ - "JHub Set-up" - ] + "text": "Changing the VM size\nNOT WORKING YET I am stuck on creating the persistent volumes. Needed because you need the user storage somewhere if you have multiple node pools.\n\nkubectl get nodes --show-labels | grep instance-type\nbeta.kubernetes.io/instance-type=Standard_D8s_v3" }, { - "objectID": "posts/Set-up-daskhub.html#create-a-separate-disk-for-user-data", - "href": "posts/Set-up-daskhub.html#create-a-separate-disk-for-user-data", + "objectID": "Set-up-daskhub.html#create-a-separate-disk-for-user-data", + "href": "Set-up-daskhub.html#create-a-separate-disk-for-user-data", "title": "DaskHub Set-up", "section": "Create a separate disk for user data", - "text": "Create a separate disk for user data\nI want the user data to be in a drive different from the VM being spun up for their notebook. Sounds easy here https://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html but I cannot string the steps together.\nSteps, I think?\n\nCreate disk\nSomething like this?\nhttps://bluexp.netapp.com/blog/azure-cvo-blg-azure-kubernetes-service-configuring-persistent-volumes-in-aks\nBut I can’t figure out the steps.\n\n\nPVC\nNOT WORKING YET\n\nIs this pvc.yaml right?\nHow would I point this to the disk that I mount in the step above??\n\nThis command might have useful info\nKUBE_EDITOR=\"nano\" kubectl edit pvc --namespace=dhub claim-eeholmes\nnana pvc.yaml\nkind: PersistentVolumeClaim\napiVersion: v1\nmetadata:\n name: hub-db-dir\n labels:\n component: jupyter\nspec:\n storageClassName: \"standard\" # name of storage class, it will be default storage class if unspecified.\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: \"40Gi\"\nkubectl create -f pvc.yaml\nTo delete, you need to first edit the pvc yaml file and get rid of pvc protection. It is 2 lines.\nkubectl --namespace=dhub get pvc\nKUBE_EDITOR=\"nano\" kubectl edit pvc --namespace=dhub claim-eeholmes\nThen you can delete\nkubectl --namespace=dhub delete pvc claim-eeholmes\nCheck that it is gone\nkubectl --namespace=dhub get pvc\nif not try\nkubectl --namespace=dhub delete pvc claim-eeholmes --grace-period=0 --force\n\n\nPV\nNeed a persistent volume claim too….\n\n\nTell the hub about the disk\nhttps://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html\nBut see how this is done on the Openscapes 2i2c hub https://github.com/2i2c-org/infrastructure/blob/master/config/clusters/openscapes/common.values.yaml\nI know their set-up is a little different: basehub -> jupyterhub in the helm chart, but I don’t see how the singleuser bit in the yaml file is referencing the nfs in the top of that yaml.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Create a separate disk for user data\nI want the user data to be in a drive different from the VM being spun up for their notebook. Sounds easy here https://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html but I cannot string the steps together.\nSteps, I think?\n\nCreate disk\nSomething like this?\nhttps://bluexp.netapp.com/blog/azure-cvo-blg-azure-kubernetes-service-configuring-persistent-volumes-in-aks\nBut I can’t figure out the steps.\n\n\nPVC\nNOT WORKING YET\n\nIs this pvc.yaml right?\nHow would I point this to the disk that I mount in the step above??\n\nThis command might have useful info\nKUBE_EDITOR=\"nano\" kubectl edit pvc --namespace=dhub claim-eeholmes\nnana pvc.yaml\nkind: PersistentVolumeClaim\napiVersion: v1\nmetadata:\n name: hub-db-dir\n labels:\n component: jupyter\nspec:\n storageClassName: \"standard\" # name of storage class, it will be default storage class if unspecified.\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: \"40Gi\"\nkubectl create -f pvc.yaml\nTo delete, you need to first edit the pvc yaml file and get rid of pvc protection. It is 2 lines.\nkubectl --namespace=dhub get pvc\nKUBE_EDITOR=\"nano\" kubectl edit pvc --namespace=dhub claim-eeholmes\nThen you can delete\nkubectl --namespace=dhub delete pvc claim-eeholmes\nCheck that it is gone\nkubectl --namespace=dhub get pvc\nif not try\nkubectl --namespace=dhub delete pvc claim-eeholmes --grace-period=0 --force\n\n\nPV\nNeed a persistent volume claim too….\n\n\nTell the hub about the disk\nhttps://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html\nBut see how this is done on the Openscapes 2i2c hub https://github.com/2i2c-org/infrastructure/blob/master/config/clusters/openscapes/common.values.yaml\nI know their set-up is a little different: basehub -> jupyterhub in the helm chart, but I don’t see how the singleuser bit in the yaml file is referencing the nfs in the top of that yaml." }, { - "objectID": "posts/Set-up-daskhub.html#troubleshooting", - "href": "posts/Set-up-daskhub.html#troubleshooting", + "objectID": "Set-up-daskhub.html#troubleshooting", + "href": "Set-up-daskhub.html#troubleshooting", "title": "DaskHub Set-up", "section": "Troubleshooting", - "text": "Troubleshooting\n\nI cannot clone repos in the JupyterHub. Restart the server. In Jupyter, File > Hub Control Panel > Stop My Server.", - "crumbs": [ - "JHub Set-up" - ] + "text": "Troubleshooting\n\nI cannot clone repos in the JupyterHub. Restart the server. In Jupyter, File > Hub Control Panel > Stop My Server." }, { - "objectID": "posts/Set-up-daskhub.html#refs-i-used", - "href": "posts/Set-up-daskhub.html#refs-i-used", + "objectID": "Set-up-daskhub.html#refs-i-used", + "href": "Set-up-daskhub.html#refs-i-used", "title": "DaskHub Set-up", "section": "Refs I used", - "text": "Refs I used\n\nOverall\n\nhttps://2i2c.org/service/#getahub\nOpenscapes common.values.yaml https://github.com/2i2c-org/infrastructure/blob/master/config/clusters/openscapes/common.values.yaml\nhttps://artifacthub.io/packages/helm/dask/daskhub\nhttps://github.com/zonca/jupyterhub-deploy-kubernetes-jetstream/blob/master/dask_gateway/dask-hub/config_daskhub.yaml\nhttps://saturncloud.io/blog/how-to-setup-jupyterhub-on-azure/\nhttps://saturncloud.io/blog/jupyterhub-and-azure-ad/\n\n\n\nStorage\n\nhttps://www.youtube.com/watch?v=Da1qn7-RHvY\nDynamic NFS provisioning 2 https://www.youtube.com/watch?v=DF3v2P8ENEg&t=0s\nDynamic NFS provisioning 1 https://www.youtube.com/watch?v=AavnQzWDTEk&t=0s\nhttps://alan-turing-institute.github.io/hub23-deploy/\nhttps://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html\nhttps://learn.microsoft.com/en-us/azure/aks/azure-nfs-volume\nhttps://learn.microsoft.com/en-us/azure/storage/files/storage-files-quick-create-use-linux\nhttps://bluexp.netapp.com/blog/azure-cvo-blg-azure-kubernetes-service-configuring-persistent-volumes-in-aks", - "crumbs": [ - "JHub Set-up" - ] + "text": "Refs I used\n\nOverall\n\nhttps://2i2c.org/service/#getahub\nOpenscapes common.values.yaml https://github.com/2i2c-org/infrastructure/blob/master/config/clusters/openscapes/common.values.yaml\nhttps://artifacthub.io/packages/helm/dask/daskhub\nhttps://github.com/zonca/jupyterhub-deploy-kubernetes-jetstream/blob/master/dask_gateway/dask-hub/config_daskhub.yaml\nhttps://saturncloud.io/blog/how-to-setup-jupyterhub-on-azure/\nhttps://saturncloud.io/blog/jupyterhub-and-azure-ad/\n\n\n\nStorage\n\nhttps://www.youtube.com/watch?v=Da1qn7-RHvY\nDynamic NFS provisioning 2 https://www.youtube.com/watch?v=DF3v2P8ENEg&t=0s\nDynamic NFS provisioning 1 https://www.youtube.com/watch?v=AavnQzWDTEk&t=0s\nhttps://alan-turing-institute.github.io/hub23-deploy/\nhttps://z2jh.jupyter.org/en/latest/jupyterhub/customizing/user-storage.html\nhttps://learn.microsoft.com/en-us/azure/aks/azure-nfs-volume\nhttps://learn.microsoft.com/en-us/azure/storage/files/storage-files-quick-create-use-linux\nhttps://bluexp.netapp.com/blog/azure-cvo-blg-azure-kubernetes-service-configuring-persistent-volumes-in-aks" }, { - "objectID": "posts/Set-up-daskhub.html#setting-up-a-shared-data-disk", - "href": "posts/Set-up-daskhub.html#setting-up-a-shared-data-disk", + "objectID": "Set-up-daskhub.html#setting-up-a-shared-data-disk", + "href": "Set-up-daskhub.html#setting-up-a-shared-data-disk", "title": "DaskHub Set-up", "section": "Setting up a shared data disk", - "text": "Setting up a shared data disk\n\nhttps://www.mathworks.com/help/matlab/import_export/work-with-remote-data.html\nhttps://realpython.com/storing-images-in-python/", - "crumbs": [ - "JHub Set-up" - ] + "text": "Setting up a shared data disk\n\nhttps://www.mathworks.com/help/matlab/import_export/work-with-remote-data.html\nhttps://realpython.com/storing-images-in-python/" }, { - "objectID": "posts/Set-up-daskhub.html#s3-access", - "href": "posts/Set-up-daskhub.html#s3-access", + "objectID": "Set-up-daskhub.html#s3-access", + "href": "Set-up-daskhub.html#s3-access", "title": "DaskHub Set-up", "section": "S3 access", - "text": "S3 access\n\nhttps://s3fs.readthedocs.io/en/latest/\nhttps://stackoverflow.com/questions/67259323/jupyterhub-access-aws-s3\nhttps://data.lpdaac.earthdatacloud.nasa.gov/s3credentialsREADME", - "crumbs": [ - "JHub Set-up" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html", - "href": "posts/JHub-User-Guide.html", - "title": "JHub User Instructions", - "section": "", - "text": "I have set us up a JupyterHub/RStudio cloud-computing hub on Azure. It’s on Kubernetes and will spin up VMs as needed. The VMs are not huge: 2CPU & 8 Gig RAM.\nhttps://jhub.opensci.live/hub/login\nLogin authentication is via GitHub. Only members of the JHub GitHub team can log on. This is a testing environment. Contact Eli if you want to test it out.\nInstructions: It should be pretty self-explanatory.", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#login", - "href": "posts/JHub-User-Guide.html#login", - "title": "JHub User Instructions", - "section": "Login", - "text": "Login\nAfter you get paste the login page, you will see this. Choose Python for Python only; choose R for Python and R.", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#spin-up-your-server", - "href": "posts/JHub-User-Guide.html#spin-up-your-server", - "title": "JHub User Instructions", - "section": "Spin up your server", - "text": "Spin up your server\nYou will see this as your server spins up", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#choose-your-platform", - "href": "posts/JHub-User-Guide.html#choose-your-platform", - "title": "JHub User Instructions", - "section": "Choose your platform", - "text": "Choose your platform\nYou can code in RStudio, JupyterLab or Terminal.", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#lets-choose-rstudio", - "href": "posts/JHub-User-Guide.html#lets-choose-rstudio", - "title": "JHub User Instructions", - "section": "Let’s choose RStudio", - "text": "Let’s choose RStudio\n\n\nClone a repo\nChoose ‘new project’ (top right) and Version Control.\n\n\n\nTell Git who you are\nTell Git who you are and save your authentication info. You only do this once (or until your PAT expires). Run this code from the R console (not terminal).\nusethis::use_git_config(user.name = \"YourName\", user.email = \"your@email.com\")\nNow create a personal access token for authentication. SAVE the token because you will need it in the next step.\nusethis::create_github_token() \nNow run this and paste in the token.\ngitcreds::gitcreds_set()\nRestart R. You can chose your project from the dropdown on the top right to do this.\nNow commit a change and push.\nNote, you can also run the commands below from a terminal window.\ngit config --global user.name \"YourName\"\ngit config --global user.email \"your@email.com\"\ngit config --global credential.helper store", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#lets-choose-jupyterlab", - "href": "posts/JHub-User-Guide.html#lets-choose-jupyterlab", - "title": "JHub User Instructions", - "section": "Let’s choose JupyterLab", - "text": "Let’s choose JupyterLab\nAny of the browser tabs with the Jupyter icon are JupyterLab.\nBe careful because you can be in RStudio with one GitHub repository and you could open the same repo in JupyterLab and easily create merge conflicts. Just be aware that they are in separate file systems so changes on RStudio will not be reflected in JupyterLab. It is not like you are on one computer.\n\nTell Git who you are\nBut I just did that with RStudio! I know but the JLab instance is in a different environment and doesn’t know what you did in the RStudio environment.\nOpen a terminal. You do this from the Launcher window. You can always open a new launcher window by clicking the little + tab\n\nNow click Terminal and run this code\ngit config --global user.name \"YourName\"\ngit config --global user.email \"your@email.com\"\ngit config --global credential.helper store\n\nCreate a PAT or use the one you created for RStudio\nMake a commit and push with the PAT as the password. Now you are set (until your PAT expires).\n\n\n\nClone a Git repo\nClick the Git icon on the left and you can clone a repo.", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/JHub-User-Guide.html#stop-your-server", - "href": "posts/JHub-User-Guide.html#stop-your-server", - "title": "JHub User Instructions", - "section": "Stop your server", - "text": "Stop your server\nIt will stop on it’s own after awhile, but if it hangs, you can stop it and restart it. With File > Hub Control Panel", - "crumbs": [ - "JHub User Guide" - ] - }, - { - "objectID": "posts/Setup-Notes.html", - "href": "posts/Setup-Notes.html", - "title": "Instructions for editing config", - "section": "", - "text": "Instructions for editing config\n\nLog into https://portal.azure.com/ and once successful, you will see this\n\n\n\nClick the JupyterHub icon and you will see this\n\n\n\nClick the Connect icon and you will see this. Ignore everything else that you see. I don’t think you need to run the kubectl get deployments --all-namespaces=true unless you need to check Kubernetes set up.\n\n\n\nType nano config.yaml to get the the JupyterHub config. This is the only file you need to change. cntl-O to write. cntl-X to exit.\n\nAfter you update the config.yaml, you need to tell the JupyterHub about the change\nhelm upgrade --cleanup-on-fail jhub jupyterhub/jupyterhub --namespace jhub --version=2.0.0 --values config.yaml\nIf upgrade was successful, you will see this (plus a bunch of text below that you can ignore).\n\n\nWhat a few minutes for your changes to take effect." + "text": "S3 access\n\nhttps://s3fs.readthedocs.io/en/latest/\nhttps://stackoverflow.com/questions/67259323/jupyterhub-access-aws-s3\nhttps://data.lpdaac.earthdatacloud.nasa.gov/s3credentialsREADME" }, { "objectID": "index.html", "href": "index.html", "title": "NMFS OpenSci JupyterHub Notes", "section": "", - "text": "This page shows my notes for setting up JupyterHubs on various platforms. What is a JupyterHub? Read about why cloud computing environments are great: SnowEx 2022", - "crumbs": [ - "Home" - ] + "text": "This page shows my notes for setting up JupyterHubs on various platforms. What is a JupyterHub? Read about why cloud computing environments are great: SnowEx 2022" }, { "objectID": "index.html#test-jhub", "href": "index.html#test-jhub", "title": "NMFS OpenSci JupyterHub Notes", "section": "Test JHub", - "text": "Test JHub\nI have set us up a JHub with RStudio on Azure. It’s on Kubernetes and will spin up VMs as needed. The VMs are not huge: 2CPU & 8 Gig RAM.\nhttps://jhub.opensci.live/hub/login\nContact Eli if you want to test it out. See instructions under JHub User Guide", - "crumbs": [ - "Home" - ] + "text": "Test JHub\nI have set us up a JHub with RStudio on Azure. It’s on Kubernetes and will spin up VMs as needed. The VMs are not huge: 2CPU & 8 Gig RAM.\nhttps://jhub.opensci.live/hub/login\nContact Eli if you want to test it out. See instructions under JHub User Guide" }, { "objectID": "index.html#installation-instructions", "href": "index.html#installation-instructions", "title": "NMFS OpenSci JupyterHub Notes", "section": "Installation instructions", - "text": "Installation instructions\nThe other tabs to the left are my JHub installation notes for various platforms.", - "crumbs": [ - "Home" - ] + "text": "Installation instructions\nThe other tabs to the left are my JHub installation notes for various platforms." }, { "objectID": "ci/iorocker/instructions.html", @@ -753,5 +334,257 @@ "title": "Indian Ocean Summer Docker Images", "section": "Adding packages with newpackages.yml", "text": "Adding packages with newpackages.yml\nWhen the openscapes image is used, we are in a conda env called ‘notebook’. We want to update that with the packages in newpackages.yml but need to get that file into the container. For now, I just hard code in the pip install commands.\nAdd to Docker file\n# it can't find new.yml in home/joyvan/.kernels\n# need to get that into the container somehow (git clone?)\n# RUN conda env update --file new.yml" + }, + { + "objectID": "ci/iopython/instructions.html", + "href": "ci/iopython/instructions.html", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "", + "text": "https://hub.docker.com/repository/docker/eeholmes/iopython/general\nThe one to use is the dated one. The main tag doesn’t seem to always be recognized as a new tag when it changes." + }, + { + "objectID": "ci/iopython/instructions.html#to-set-docker-tag-to-latest-commit", + "href": "ci/iopython/instructions.html#to-set-docker-tag-to-latest-commit", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "To set docker tag to latest commit", + "text": "To set docker tag to latest commit\nSHA7=\"$(git rev-parse --short HEAD)\"\nDOCKER_TAG=$SHA7\nI am not doing that since this repo has lots of commits unrelated to the docker image." + }, + { + "objectID": "ci/iopython/instructions.html#to-set-up-your-own-docker-repo", + "href": "ci/iopython/instructions.html#to-set-up-your-own-docker-repo", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "To set up your own Docker repo", + "text": "To set up your own Docker repo\n\nMake an account on DockerHub. Free is fine.\nCreate a repo and give it a name. For example, for this project, my account is eeholmes and my repo is iopython (Indian Ocean Python) as it is specific a particular project I am working on.\n\nDockerHub will want to you to buy the premium account but you only need that if you are doing continuous integration, like using a GitHub Action to autobuild your image. If you are manually building, you don’t need this." + }, + { + "objectID": "ci/iopython/instructions.html#why-is---platform-needed-in-the-build-command", + "href": "ci/iopython/instructions.html#why-is---platform-needed-in-the-build-command", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "Why is --platform needed in the build command", + "text": "Why is --platform needed in the build command\nYou won’t see this on docker build tutorials. But if you are on a Mac with Apple chip, then you’ll build arm64 images and that’s not going to work on Ubuntu VMs. The vanilla images you see are amd64 so we want to make sure we are building for that platform. This only matters if you are on a Mac with Apple chip, but it won’t break things for unix and PC so I added to make the instructions more robust." + }, + { + "objectID": "ci/iopython/instructions.html#if-a-specific-image-tag-is-in-config", + "href": "ci/iopython/instructions.html#if-a-specific-image-tag-is-in-config", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "If a specific image tag is in config", + "text": "If a specific image tag is in config\nThe JupyterHub has a config file that specifies what images are being used. If the image is say eeholmes/iopython:hublatest, then whenever the a image with tag hublatest is pushed, the hub will use that. If on the otherhand, you config file has a specific, an unique tag that you don’t overwrite, then you’ll have to update the file in the config file on the cluster (log into Azure, go to cluster, connect to cloud shell, nano dconfig2.yaml) and upgrade the installation of the JupyterHub.\nWhy not eeholmes/iopython:latest? There is nothing special about latest. It is the default tag used if you don’t specify -t and : in your build call. So it is a bit too easy to accidentally update “latest” and thus update the image for you hub when you didn’t intend to do that. You just forgot to specify a tag.\nTo update if you are using a specific tag, like 20230615 rather than one you keep updating like hublatest or latest:\n\nStep 1\nEdit the config file. Mine is called dconfig2.yaml. Yours is probably config.yaml. Name is unimportant.\nnano dconfig2.yaml\nInside dconfig2.yaml is this info. This shows a fixed tag. So if I update, I need to change the 20230615 part.\n singleuser:\n image:\n name: eeholmes/iopython\n tag: 20230615\nSave the changes. In nano, it cmd-O, return, cmd-X.\n\n\nrun helm upgrade\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\n\n\nThe helm upgrade command\nhelm upgrade --cleanup-on-fail --render-subchart-notes dhub dask/daskhub --namespace dhub --version=2023.1.0 --values dconfig2.yaml\nA helm is what runs the commands to upgrade (and install in the beginning) our JupyterHub. dask/daskhub is point to the repo with the “helm chart” (the instructions). --value dconfig2.yaml is telling it where the config file is.\n\nupgrade upgrade an existing installation with the values in dconfig2.yaml\n--render-subchart-notes the dask/daskhub helm chart has subcharts (jupyterhub) and you need to render these too. Not all helm charts have this.\ndask/daskhub the name of the repo that has the helm chart. The first time you reference this, you need to tell help about the repo by giving it the url. Read how here\n--version=2023.1.0 version of the helm chart. Update when the helm chart (instructions for installing the jupyterhub) changes." + }, + { + "objectID": "ci/iopython/instructions.html#adding-packages-with-newpackages.yml", + "href": "ci/iopython/instructions.html#adding-packages-with-newpackages.yml", + "title": "Indian Ocean Summer Docker Images: Openscapes + a few extras", + "section": "Adding packages with newpackages.yml", + "text": "Adding packages with newpackages.yml\nWhen the openscapes image is used, we are in a conda env called ‘notebook’. We want to update that with the packages in newpackages.yml but need to get that file into the container. For now, I just hard code in the pip/conda install commands.\nAdd to Docker file\n# it can't find new.yml in home/joyvan/.kernels\n# need to get that into the container somehow (git clone?)\n# RUN conda env update --file new.yml" + }, + { + "objectID": "Set-up-centos.html", + "href": "Set-up-centos.html", + "title": "Centos Set-up", + "section": "", + "text": "This is my notes for setting this up on a Centos 8 (Linux distribution) server. Jump to the “Summary” section to see only the instructions without explanations.\nAll the commands are run in a shell (bash)\nReferences:" + }, + { + "objectID": "Set-up-centos.html#set-up-vm-on-azure", + "href": "Set-up-centos.html#set-up-vm-on-azure", + "title": "Centos Set-up", + "section": "Set up VM on Azure", + "text": "Set up VM on Azure\n\nCreated a Centos 8.3 server on Azure: https://portal.azure.com/#create/cloud-infrastructure-services.centos-8-3centos-8-3\nI didn’t do anything special for set-up. Choose SSH with key.\nOnce it is created, I went to the dashboard and selected my VM. The dashboard has a “Connect” button to get to the shell and it shows the public IP address.\nI had to create a special security rule to allow me to ssh into the public IP address to connect. Normally I use the cloud shell to connect, but Azure would not let me connect via the cloud shell for a server since it wanted upgraded security package and I cannot do that with my work subscription.\nThen I saved the key somewhere on my computer and\n\n\nchmod 400 ~/<key location>\nssh -i ~/<key location>/Centos8.cer <vm-username>@<public ip-address>" + }, + { + "objectID": "Set-up-centos.html#on-vm-check-set-up", + "href": "Set-up-centos.html#on-vm-check-set-up", + "title": "Centos Set-up", + "section": "On VM check set-up", + "text": "On VM check set-up\nI ssh-ed into the VM with\n\nssh -i <path to key downloaded from Azure> eeholmes@<public ip address>\n\n\nMake sure you are root\nGetting the JupyterHub set up needs to be done as root. First make sure you have an admin password. When I set up my Azure VM, I did not set a password. So first\n\nsudo passwd <your username>\n\nand set a password. Then switch to root if you are not signed in as root\n\nsudo -i\n\n\n\nCheck for Python\nYou will need Python 3.6+ installed. Open a terminal window and run python3 --version or python --version to see if Python is installed and what the version is.\nCheck your operating system (OS) with\n\ncat /etc/os-release\n\n\n\nCheck for conda\nYou will need conda (or miniconda) for these instructions. conda (and miniconda) take care of checking that all our packages will be inter-operable. It is best to install JupyterHub into a clean environment. That way you minimize chances of conflicts and your environment will solve (figure out any conflicts) much much faster.\nCheck for conda with\n\nconda list\n\nIf it doesn’t show a list of environments, then you need to install miniconda. Installation instructions. Read about miniconda for scientists from Software Carpentries here.\nThis is what I used to install miniconda from these instructions. Note install miniconda in some place like /opt/miniconda3 where all users will have access to `/opt/miniconda3/bin. We don’t want to install in /root/ for example or the admin users home directory.\n\nmkdir -p /opt/miniconda3\nwget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /opt/miniconda3/miniconda.sh\nbash /opt/miniconda3/miniconda.sh -b -u -p /opt/miniconda3\nrm -rf /opt/miniconda3/miniconda.sh\n\nThen initialize to set up the path. Note I am using bash. You’ll need to change if you are using zsh.\n\n/opt/miniconda3/bin/conda init bash\nsource ~/.bashrc\n\nnote will need to do something else to add the conda binary to all the users’ paths" + }, + { + "objectID": "Set-up-centos.html#create-the-conda-environment", + "href": "Set-up-centos.html#create-the-conda-environment", + "title": "Centos Set-up", + "section": "Create the conda environment", + "text": "Create the conda environment\nCreate the conda environment for the jupyterhub installation. Installation will be in a directory with all the files for packages. Then activate it (enter it), and get the location of the environment (folder).\nAll the commands below are in the terminal window on your VM/server.\nCreate the environment named jupyterhub with python and jupyterhub (module). After creating, activate (enter) that environment. Then install jupyterlab, notebook and dockerspawner into the environment. Note the jupyterhub after -n is the name of the environment.\n\nconda create -n jupyterhub python\n\nThen activate (enter) that environment\n\nconda activate jupyterhub\n\nThen install jupyterhub here\n\nconda install -c conda-forge jupyterhub\n\nand then jupyterlab\n\nconda install -c conda-forge jupyterlab notebook\n\n\nSet a variable for env path\nThe environment has a folder with all the packages and binaries that we install. We are going to need to know the location of that folder. Get the location with\n\nconda env list\n\nOn the VM I set up, the folder location is\n\n/opt/miniconda3/envs/jupyterhub\n\nYours could be something entirely different. On another server with anaconda (a not-free conda package resolver), the folder was\n\n/SHARE/anaconda3/envs/jupterhub/\n\nWe are going to be saving the configuration files for our JupyterHub in this folder. Let’s save the path to a variable so we don’t have to keep entering the whole path.\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\n\nMake sure users can read and execute this folder. They need to in order to be able to spawn instances for the hub.\n\nchmod 755 $JHUBENV\n\nYou should now be able to start the hub, but you will not be able to access it yet because you need to open the 8000 port. Type\n\n$JHUBENV/bin/jupyterhub\n\nand check that it starts. Then use Cntl-C to stop the hub." + }, + { + "objectID": "Set-up-centos.html#create-a-user-on-the-vm", + "href": "Set-up-centos.html#create-a-user-on-the-vm", + "title": "Centos Set-up", + "section": "Create a user on the VM", + "text": "Create a user on the VM\nBy default, any user on the server will be able to login. Let’s create a test user so that we are not logging into our hub with the root user password. We will be using “http” until we secure it so passwords are potentially exposed.\n\nuseradd jhub\n\nand give it a password when it asks." + }, + { + "objectID": "Set-up-centos.html#open-the-8000-port", + "href": "Set-up-centos.html#open-the-8000-port", + "title": "Centos Set-up", + "section": "Open the 8000 port", + "text": "Open the 8000 port\nFirewallD was not running on my Azure Centos server, so I started it up to manage the ports.\n\nsudo systemctl enable firewalld\nsudo systemctl start firewalld\n\nFind out the Public IP address for the server you are on; it’s listed on the Azure overview and networking page for the VM in the Azure portal. Then open the 8000 port.\nFirst find out what ports are open through the firewall\n\nsudo firewall-cmd --list-ports\n\nAdd the 8000 port, reload and recheck that it appears.\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nBecause I am on an Azure VM, I also have to set up a networking rule to allow the 8000 port. By default, all public access to the server is blocked. Go to the Azure dashboard, select your VM, then select Networking under Settings, and then click Add Inbound Port rule. I am pretty sure you need to select “http” instead of “https”.\nOnce the port is open, you should be able to reach your JupyterHub at http://XXX.XX.XX.XX:8000 (replace the XX’s with the Public IP address).\nBackground\nThe JupyterhHub is running by default on http://localhost:8000. This means that if you start the hub on a machine that you are logged into, you should be able to open a browser on that machine, enter http://localhost:8000 and the hub login page will appear. There are a few reasons that might not work\n\nYou are ssh-ing into a server and don’t have a browser to open. The browser on the computer that you are ssh-ing from is the “localhost” in this case and you need the “localhost” to be the server.\nYou are logged directly into your server, but it doesn’t have a browser installed.\n\nHowever http://localhost:8000 is actually not very useful. We are trying to create a hub that others can log into from their browsers.\nSo you need to determine the Public IP address for the server you are on. This is the IP address that you could enter into a browser. If you enter http://XXX.XX.XX.XX (replace with actual IP), then you should see a page of some sort. This indicates that the server is working. If you are on an internal network, then you will only be able to load the address if you are also on that network. But for security reason, ports will not be open by default. You need to open the 8000 port so that http://XXX.XX.XX.XX:8000 will be found." + }, + { + "objectID": "Set-up-centos.html#log-in", + "href": "Set-up-centos.html#log-in", + "title": "Centos Set-up", + "section": "Log in!", + "text": "Log in!\nAt this point, you should be able to login with the jhub test account." + }, + { + "objectID": "Set-up-centos.html#set-up-a-configuration-file", + "href": "Set-up-centos.html#set-up-a-configuration-file", + "title": "Centos Set-up", + "section": "Set up a configuration file", + "text": "Set up a configuration file\nSo far, we have started the hub with the default configuration. We are going to need to customize it. For that we need a configuration file. We will create this in the folder where the environment files are.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\n\nNext create the default configuration file jupyterhub_config.py.\n\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nBecause we cd-d into the $JHUBENV/etc/jupyterhub/ directory, the file is created there. This default file is very long. Open up with\n\nnano jupyterhub_config.py\n\nUse F6 to find lines. Uncomment these two lines and save (Cntl-O, Enter, Cntl-X).\n\nc.Spawner.http_timeout = 3600" + }, + { + "objectID": "Set-up-centos.html#make-a-new-server-service", + "href": "Set-up-centos.html#make-a-new-server-service", + "title": "Centos Set-up", + "section": "Make a new server service", + "text": "Make a new server service\n\nCreate the new unit file\nAt this point, after opening the port, you should be able to get to your JupyterHub by starting it with jupyterhub --ip XXX.XX.XX.XX --port=8000 and then browsing to http://XXX.XX.XX.XX:8000. But you hub is going to be stopped whenever the server is rebooted. So next we need to set up a service for your service so that our hub starts automatically.\nCreate a new directory for the service unit file,\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\n\nCreate the file and name jupyterhub.service. For example, using nano editor, we do\n\nnano jupyterhub.service\n\nAnd into that file we put the following. Replace /opt/miniconda3/envs/jupyterhub with the actual path to the jupyterhub environment folder.\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nNext we make systemd aware of the new service.\nCreate a symlink file in the folder where all the server services are kept. And tell systemd to reload its configuration files\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\n\n\n\nMake sure SELinux doesn’t block our service\nSELinux (security for the server) checks that files that are used have the correct label. All our files have generic file labels. If you do,\n\nls -Z $JHUBENV/etc/systemd/\n\nYou will see that the file label is unconfined_u:object_r:usr_t:s0. We need it to be\n\nsystemd_unit_file_t\n\nWe change the file label with\n\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\n\nSELinux will also object to the file label on all the binaries that we use to start up the JupyterHub (like jupyterhub) so we need to fix those file labels.\nThis will add bin_t label to all the binaries and check that it worked.\n\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\nls -Z $JHUBENV/bin\n\nIt got all the binaries but not the simlinks. Nonetheless it seemed to run ok.\n\n\nEnable our new service\n\nsudo systemctl enable jupyterhub.service\n\nThe service will start on reboot, but we can start it straight away using start:\n\nsudo systemctl start jupyterhub.service\n\nCheck that it is running.\n\nsudo systemctl status jupyterhub.service\n\nIf it fails, try\n\naudit2why < /var/log/audit/audit.log\n\nto debug. It is likely to be an issue with SELinux blocking the service from starting.\nNow our hub should be available on http:\\\\XXX.XX.XX.XX:8000. You can double check that it is listen on this port by running\n\nnetstat -tuln\n\nAt this point, you will need to address security if your hub is open to the web, as opposed to being on an internal network and only accessible to that network. Learn about that here." + }, + { + "objectID": "Set-up-centos.html#set-up-docker-for-user-environment", + "href": "Set-up-centos.html#set-up-docker-for-user-environment", + "title": "Centos Set-up", + "section": "Set up Docker for user environment", + "text": "Set up Docker for user environment\nWhen you log in the jupyter notebooks will be trying to use the Python environment that was created to install JupyterHub, this is not what we want. We will use a docker image to “spawn” the user environment. Read here for other approaches.\nWe are going to use dockerspawner so that we can use a docker image for our user environments. The user will work in these containerized environments and they won’t have access to any other files in the server. In order to share their work with others, the normal workflow would be to work in Git repos and share those repos to a GitHub (or GitLab server). Each user will have a home directory on the server for their files, but they won’t have access to other hub user directories nor will they have access to any other directories on the server.\n\nInstall docker\nI am using Centos in this example\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nThen we need to start docker\n\nsudo systemctl start docker\n\n\n\nInstall dockerspawner\nI am going to be creating the user environment from a docker image, so I also want dockerspawner. Note dockerspawner installed docker-py but it was really old and threw errors so I installed separately to get the latest version. Note make sure you are in the jupyterhub conda env. You can run conda env list and use conda activate jupyterhub if you are not in it.\n\nconda install -c conda-forge dockerspawner\nconda install -c conda-forge docker-py\n\n\n\nJupyter images\nThe image that we use must have the jupyterhub and notebook module installed. The jupyterhub version needs to also match what you have on your hub.\nCheck the version on your server:\n\n$JHUBENV/bin/jupyterhub -V\n\nFor demo purposes, we will use the jupyter images on DockerHub. We want to find an image with the same version of jupyterhub as we have on our server.\n\n\nEdit the config file\nEdit the jupyterhub_config.py file in $JHUB-ENV/etc/jupyterhub/ to add that we want to use DockerSpawner and specify the images that users should have access to. Users will get a drop down menu. Add these lines to jupyterhub_config.py. The hub bind url needs to be 0.0.0.0 because we are using a docker container for the individual user environments.\n\nhttps://discourse.jupyter.org/t/whats-the-main-difference-between-hub-connect-url-vs-hub-bind-url/3596/2\nNote image_whitelist is deprecated as of dockerspawner 12.0. New name is allowed_images.\n\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nDo a docker pull of the images so that they don’t have to be pulled the first time that a user chooses that image.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nNow you can restart the service and the user can start a notebook with the specified images.\n\n\nCreate your own Docker images\nDocker images that work with JupyterHub with Kubernetes will work with this set-up with the addition of jupyterhub and notebook.\nAdd the following to your Docker image\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using rocker image. Code added to make the home directory home/jovyan.\n\nFROM rocker/binder:4.3\n\nUSER root\nRUN usermod -d /home/jovyan rstudio\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nWORKDIR /home/jovyan\n\nCMD [\"jupyterhub-singleuser\"]\n\nExample using openscapes/rocker\n\nFROM openscapes/rocker:a7596b5\n\nRUN pip3 install \\\n 'jupyter-rsession-proxy' \\\n 'jupyterhub==3.1.*' \\\n 'notebook==6.*' \\\n 'jupyterlab'\n\nUSER root\nRUN mkdir /home/jovyan\nRUN chown rstudio:rstudio /home/jovyan\nUSER rstudio\n\nCMD [\"jupyterhub-singleuser\"]\n\n\n\nSpecial note regarding rocker images\nThe default home directory for rocker images is home/rstudio but the default for JupyterHub is home/jovyan." + }, + { + "objectID": "Set-up-centos.html#persistent-volume", + "href": "Set-up-centos.html#persistent-volume", + "title": "Centos Set-up", + "section": "Persistent volume", + "text": "Persistent volume\nAdd the following to the config file to create a persistent volume.\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }" + }, + { + "objectID": "Set-up-centos.html#user-environment-customization", + "href": "Set-up-centos.html#user-environment-customization", + "title": "Centos Set-up", + "section": "User environment customization", + "text": "User environment customization\n\nMemory limits and guarantees\nYou can set memory limits on the containers that are spawned for users by adding limits. Read the documentation here.\nFor example:\n\nc.DockerSpawner.mem_limit = '8G'\nc.DockerSpawner.mem_guarantee = '1G'\nc.DockerSpawner.cpu_guarantee = 0.5\nc.DockerSpawner.cpu_limit = 1\n\nIf that doesn’t work try\n\nc.Spawner.mem_limit = '2G'\n\nI believe you can specify as a drop-down to give the user choices:\n\nc.DockerSpawner.mem_guarantee = {\n '1G': '1G',\n '2G': '2G',\n '8G': '8G',\n}\n\n\n\nCreating a shared volume\nOne read-only shared volume:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\n\nc.DockerSpawner.volumes = { 'jupyterhub-user-{username}':'/home/jovyan', '/path/to/shared': {\"bind\": '/home/jovyan/shared', \"mode\": \"ro\"} }\n\nA volume that is read-only for some and read-write for others:\nhttps://github.com/jupyterhub/dockerspawner/issues/172\nMore discussions around shared volumes\nhttps://github.com/jupyterhub/dockerspawner/issues/453" + }, + { + "objectID": "Set-up-centos.html#setting-up-https", + "href": "Set-up-centos.html#setting-up-https", + "title": "Centos Set-up", + "section": "Setting up https", + "text": "Setting up https\nIf you are using a public IP address, rather than being on a private network, you need to set up https so that content (passwords and everything else) is not visible. Read how to do that here.\nThese instructions set up this url: https:\\\\dhub.bluemountain123.live" + }, + { + "objectID": "Set-up-centos.html#github-authentication", + "href": "Set-up-centos.html#github-authentication", + "title": "Centos Set-up", + "section": "GitHub authentication", + "text": "GitHub authentication\nAfter you have https set-up, we can set up authentication via a GitHub teams in a GitHub organization. Read other ways to authenticate (create users) here.\nhttps://oauthenticator.readthedocs.io/en/latest/tutorials/provider-specific-setup/providers/github.html\n\nCreate a new Oauth Application on GitHub\nThis Oauth application is going to be associated with your (personal) GitHub account, but you will use a team on a GitHub organization that you are owner of for the users who are allowed to log into your JupyterHub.\nLog into GitHub and go to GitHub > Settings > Developer Settings > New Oauth Application\nLook carefully at how I filled in the boxes. Change the URL and the name of the application.\n\nNext you will see something like this\n\nYou need to copy the ID and then click the create secrets button and save the secret. You will need those in the next step.\n\n\nCreate a team in your GitHub organization\nYou will be added by default and add anyone else who needs access to the hub. Let’s say your GitHub organization is MyOrg and the team is called JHub. So then the allowed organization is MyOrg:JHub. You can leave off :JHub if you want to allow all members of the organization to log in.\n\n\nEdit the jupyterhub_config.py file\nc.JupyterHub.authenticator_class = \"github\"\nc.OAuthenticator.oauth_callback_url = \"https:\\\\dhub.bluemountain123.live/hub/oauth_callback\"\nc.OAuthenticator.client_id = \"your oauth2 application id\"\nc.OAuthenticator.client_secret = \"your oauth2 application secret\"\nc.GitHubOAuthenticator.allowed_organizations = \"MyOrg:JHub\"\nc.GitHubOAuthenticator.scope = \"read:org\"\nReplace the id, secret and url with your values." + }, + { + "objectID": "Set-up-centos.html#summary", + "href": "Set-up-centos.html#summary", + "title": "Centos Set-up", + "section": "Summary", + "text": "Summary\nOnly the instructions. Make sure you are installing as the root user. I assume you have Python and conda installed.\nCreate the conda environment\n\nsudo -i\n\nconda create -n jupyterhub python --yes\nconda activate jupyterhub\nconda install -c conda-forge jupyterhub --yes\nconda install -c conda-forge jupyterlab notebook --yes\n\nJHUBENV=/opt/miniconda3/envs/jupyterhub\nchmod 755 $JHUBENV\n\nCreate user\n\nuseradd jhub\n\nOpen the 8000 port for access to the application.\n\n#sudo systemctl enable firewalld\n#sudo systemctl start firewalld\n\nsudo firewall-cmd --permanent --add-port 8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nCreate the configuration file. Will be edited at end.\n\nsudo mkdir -p $JHUBENV/etc/jupyterhub/\ncd $JHUBENV/etc/jupyterhub/\nsudo $JHUBENV/bin/jupyterhub --generate-config\n\nInstall docker if needed\n\nsudo yum install -y yum-utils\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\nsudo systemctl start docker\n\nNot sure this is needed.\n\nsudo firewall-cmd --zone=docker --add-port=8081/tcp\nsudo firewall-cmd --reload\nsudo systemctl restart docker\n\nInstall dockerspawner\n\nconda install -c conda-forge dockerspawner --yes\nconda install -c conda-forge docker-py --yes\n\nEdit the configuration file.\n\ncd $JHUBENV/etc/jupyterhub/\nnano jupyterhub_config.py\n\nPaste this in\n\n# Configuration file for jupyterhub.\n\nc = get_config() #noqa\nc.JupyterHub.port = 8000\nc.JupyterHub.hub_bind_url = \"http://0.0.0.0:8081\"\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.remove = True\nc.Spawner.http_timeout = 3600\nc.DockerSpawner.image_whitelist = {\n 'iorocker': 'eeholmes/iorocker-standalone:20231003',\n 'rocker-binder': 'eeholmes/rocker-binder:20231003',\n 'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',\n 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',\n 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',\n}\n\nnotebook_dir = '/home/jovyan'\nc.DockerSpawner.notebook_dir = notebook_dir\n\n# Mount the real user's Docker volume on the host to the notebook user's\n# notebook directory in the container\nc.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }\n\nDocker pull of the images. Do all.\n\ndocker pull jupyter/datascience-notebook:r-4.3.1\ndocker pull jupyter/scipy-notebook:7e1a19a8427f\n\nMake a new server service\n\nsudo mkdir -p $JHUBENV/etc/systemd\ncd $JHUBENV/etc/systemd\nnano jupyterhub.service\n\nPaste this in\n\n[Unit]\nDescription=JupyterHub\nAfter=syslog.target network.target\n\n[Service]\nUser=root\nEnvironment=\"PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin\"\nExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py\n\n[Install]\nWantedBy=multi-user.target\n\nMake sure SELinux doesn’t block our service\n\nls -Z $JHUBENV/etc/systemd/\nsudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service\nsudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \\;\n\nEnable our new service\n\nsudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service\nsudo systemctl daemon-reload\nsudo systemctl enable jupyterhub.service\nsudo systemctl start jupyterhub.service\n\nDone! See the long instructions if anything is not working.\nNow go through the https and GitHub authentication steps if you need that." + }, + { + "objectID": "JHub-User-Guide.html", + "href": "JHub-User-Guide.html", + "title": "JHub User Instructions", + "section": "", + "text": "I have set us up a JupyterHub/RStudio cloud-computing hub on Azure. It’s on Kubernetes and will spin up VMs as needed. The VMs are not huge: 2CPU & 8 Gig RAM.\nhttps://jhub.opensci.live/hub/login\nLogin authentication is via GitHub. Only members of the JHub GitHub team can log on. This is a testing environment. Contact Eli if you want to test it out.\nInstructions: It should be pretty self-explanatory." + }, + { + "objectID": "JHub-User-Guide.html#login", + "href": "JHub-User-Guide.html#login", + "title": "JHub User Instructions", + "section": "Login", + "text": "Login\nAfter you get paste the login page, you will see this. Choose Python for Python only; choose R for Python and R." + }, + { + "objectID": "JHub-User-Guide.html#spin-up-your-server", + "href": "JHub-User-Guide.html#spin-up-your-server", + "title": "JHub User Instructions", + "section": "Spin up your server", + "text": "Spin up your server\nYou will see this as your server spins up" + }, + { + "objectID": "JHub-User-Guide.html#choose-your-platform", + "href": "JHub-User-Guide.html#choose-your-platform", + "title": "JHub User Instructions", + "section": "Choose your platform", + "text": "Choose your platform\nYou can code in RStudio, JupyterLab or Terminal." + }, + { + "objectID": "JHub-User-Guide.html#lets-choose-rstudio", + "href": "JHub-User-Guide.html#lets-choose-rstudio", + "title": "JHub User Instructions", + "section": "Let’s choose RStudio", + "text": "Let’s choose RStudio\n\n\nClone a repo\nChoose ‘new project’ (top right) and Version Control.\n\n\n\nTell Git who you are\nTell Git who you are and save your authentication info. You only do this once (or until your PAT expires). Run this code from the R console (not terminal).\nusethis::use_git_config(user.name = \"YourName\", user.email = \"your@email.com\")\nNow create a personal access token for authentication. SAVE the token because you will need it in the next step.\nusethis::create_github_token() \nNow run this and paste in the token.\ngitcreds::gitcreds_set()\nRestart R. You can chose your project from the dropdown on the top right to do this.\nNow commit a change and push.\nNote, you can also run the commands below from a terminal window.\ngit config --global user.name \"YourName\"\ngit config --global user.email \"your@email.com\"\ngit config --global credential.helper store" + }, + { + "objectID": "JHub-User-Guide.html#lets-choose-jupyterlab", + "href": "JHub-User-Guide.html#lets-choose-jupyterlab", + "title": "JHub User Instructions", + "section": "Let’s choose JupyterLab", + "text": "Let’s choose JupyterLab\nAny of the browser tabs with the Jupyter icon are JupyterLab.\nBe careful because you can be in RStudio with one GitHub repository and you could open the same repo in JupyterLab and easily create merge conflicts. Just be aware that they are in separate file systems so changes on RStudio will not be reflected in JupyterLab. It is not like you are on one computer.\n\nTell Git who you are\nBut I just did that with RStudio! I know but the JLab instance is in a different environment and doesn’t know what you did in the RStudio environment.\nOpen a terminal. You do this from the Launcher window. You can always open a new launcher window by clicking the little + tab\n\nNow click Terminal and run this code\ngit config --global user.name \"YourName\"\ngit config --global user.email \"your@email.com\"\ngit config --global credential.helper store\n\nCreate a PAT or use the one you created for RStudio\nMake a commit and push with the PAT as the password. Now you are set (until your PAT expires).\n\n\n\nClone a Git repo\nClick the Git icon on the left and you can clone a repo." + }, + { + "objectID": "JHub-User-Guide.html#stop-your-server", + "href": "JHub-User-Guide.html#stop-your-server", + "title": "JHub User Instructions", + "section": "Stop your server", + "text": "Stop your server\nIt will stop on it’s own after awhile, but if it hangs, you can stop it and restart it. With File > Hub Control Panel" + }, + { + "objectID": "set-up-vm.html", + "href": "set-up-vm.html", + "title": "Set up VM", + "section": "", + "text": "For testing JupyterHub set-ups, I start various Linux machines. Here is how to set up a virtual machines." + }, + { + "objectID": "set-up-vm.html#azure", + "href": "set-up-vm.html#azure", + "title": "Set up VM", + "section": "Azure", + "text": "Azure\n\nCreated a Centos 8.3 server on Azure: https://portal.azure.com/#create/cloud-infrastructure-services.centos-8-3centos-8-3\nI didn’t do anything special for set-up. Choose SSH with key.\nOnce it is created, I went to the dashboard and selected my VM. The dashboard has a “Connect” button to get to the shell and it shows the public IP address.\nI had to create a special security rule to allow me to ssh into the public IP address to connect. Normally I use the cloud shell to connect, but Azure would not let me connect via the cloud shell for a server since it wanted upgraded security and I cannot do that with my work subscription.\nThen I saved the key somewhere on my computer and\n\nchmod 400 ~/<key location>\nssh -i ~/<key location> <vm-username>@<public key>\n\nI downloaded VMware Fusion 13.0.2 for M1 macs.\nThen I downloaded a Centos 9 server image from here\nhttps://www.centos.org/download/\nOpen VMWare and create a new VM. Choose other Linux. Doesn’t actually matter since it will be removed.\nShut down the VM.\nGo to settings and remove the hard drive.\nAdd a new hardrive. For me, I used ‘Add Device’ in the upper right of the Settings box. Choose ‘existing harddrive’\nHelp for M1 https://medium.com/@thehippieandtheboss/how-to-create-a-linux-virtual-machine-on-macos-1278ec1ef327\nhttps://tomcudd.com/how-i-set-up-a-centos-7-virtual-machine/" + }, + { + "objectID": "Setup-Notes.html", + "href": "Setup-Notes.html", + "title": "Instructions for editing config", + "section": "", + "text": "Instructions for editing config\n\nLog into https://portal.azure.com/ and once successful, you will see this\n\n\n\nClick the JupyterHub icon and you will see this\n\n\n\nClick the Connect icon and you will see this. Ignore everything else that you see. I don’t think you need to run the kubectl get deployments --all-namespaces=true unless you need to check Kubernetes set up.\n\n\n\nType nano config.yaml to get the the JupyterHub config. This is the only file you need to change. cntl-O to write. cntl-X to exit.\n\nAfter you update the config.yaml, you need to tell the JupyterHub about the change\nhelm upgrade --cleanup-on-fail jhub jupyterhub/jupyterhub --namespace jhub --version=2.0.0 --values config.yaml\nIf upgrade was successful, you will see this (plus a bunch of text below that you can ignore).\n\n\nWhat a few minutes for your changes to take effect." + }, + { + "objectID": "Set-up-centos-security.html", + "href": "Set-up-centos-security.html", + "title": "Set-up CentOS https", + "section": "", + "text": "Now that our basic JupyterHub is running, we want to secure it. We are going to use Let’s Encrypt. Prerequisites:\nReferences:" + }, + { + "objectID": "Set-up-centos-security.html#create-a-domain-name", + "href": "Set-up-centos-security.html#create-a-domain-name", + "title": "Set-up CentOS https", + "section": "Create a domain name", + "text": "Create a domain name\nFind a domain name provider and set one up. It is not expensive. I used GoDaddy. You only need one. Later you can use it for multiple hubs using subdomains where are created by the next step (DNS entry). For example, let’s say you get the domain bluemountain123.live. You can have as many subdomains as you want and they will be subdomain.bluemountain123.live.\n\nCreate a DNS entry\nLet’s pretend you set up bluemountain123.live as the domain. Go to the DNS settings for your domain. Add a type A record. This will do 2 things. First this will create the subdomain that you will use to access your JupyterHub. So let’s say you create, dhub as the type A DNS entry. Put dhub in the name and the public IP address of the server (leaving off :8000) in the value section. Then dhub.bluemountain123.live will be the url.\n\n\n\nTest if the url is working\nhttp:\\\\dhub.bluemountain123.live:8000 would be the url using the example domain above. Test that it is working (shows a JupyterHub login) before moving on. This is what you should see:" + }, + { + "objectID": "Set-up-centos-security.html#prep-the-server", + "href": "Set-up-centos-security.html#prep-the-server", + "title": "Set-up CentOS https", + "section": "Prep the server", + "text": "Prep the server\n\nOpen port 80\nThis is the default port for http and certbot is going to spin up a temporary webserver on this port and get the SSL certificates. We will close this port when we are done.\n\nGo to the Azure dashboard (Networking section) for your CentOS server and make sure port 80 is open.\nCheck that the firewall is not blocking port 80: sudo firewall-cmd --list-ports. If 80 is not listed, we need to add it and reload:\n\n\nsudo firewall-cmd --permanent --add-port 80/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\n\n\nStop our JupyterHub\n\nsudo systemctl start jupyterhub.service" + }, + { + "objectID": "Set-up-centos-security.html#install-certbot", + "href": "Set-up-centos-security.html#install-certbot", + "title": "Set-up CentOS https", + "section": "Install certbot", + "text": "Install certbot\nPer Let’s Encrypt recommendations, we will use certbot to get our SSL certificates. https://certbot.eff.org/.\nHere are the instructions for certbot on CentOS 8: https://certbot.eff.org/instructions?ws=other&os=centosrhel8 We choose “other” as the software.\n\nUpdate the CentOS repos\nI am using an End-of-Life CentOS distribution (sigh), and the repositories have been archived. This solution worked.\n\ndnf --disablerepo '*' --enablerepo=extras swap centos-linux-repos centos-stream-repos\ndnf distro-sync\n\nNote the last line, suggesting updating a bunch of packages and I said NO to that.\n\n\nInstall snap\nPer instructions here: https://snapcraft.io/docs/installing-snap-on-centos This updated some SELinux packages, which seemed a bit alarming but nothing seemed to break.\n\nsudo yum install snapd\nsudo systemctl enable --now snapd.socket\nsudo ln -s /var/lib/snapd/snap /snap\n\n\n\nInstall certbot\nI had to run this twice. First time it complained.\n\nsudo snap install --classic certbot\nsudo ln -s /snap/bin/certbot /usr/bin/certbot\n\n\n\nCreate the SSL certs.\nHave certbot create the SSL certs by spinning up a temporary webserver listening on port 80. Per instructions on the certbot website.\n\nsudo certbot certonly --standalone\n\nIt’ll ask for your email and the URL of your website. In my toy example, I created the domain dhub.bluemountain123.live.\n\n\nSSL cert renewal\nWith certbot running, the certificates should auto renew, but I haven’t tested this." + }, + { + "objectID": "Set-up-centos-security.html#update-the-jupyterhub-config-file", + "href": "Set-up-centos-security.html#update-the-jupyterhub-config-file", + "title": "Set-up CentOS https", + "section": "Update the JupyterHub config file", + "text": "Update the JupyterHub config file\nEdit with something like\n\ncd /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/\nnano jupyterhub_config.py\n\nThen add this to the config file. The port that is configured for SSL by default is 443. https is not going to work on 8000 which we had configured for http (on Azure).\nc.JupyterHub.port = 443\nc.JupyterHub.ssl_key = '/etc/letsencrypt/live/dhub.bluemountain123.live/privkey.pem'\nc.JupyterHub.ssl_cert = '/etc/letsencrypt/live/dhub.bluemountain123.live/fullchain.pem'\n\nRestart and Test\nWe need to open 443 in the firewall, and we can close 80 and 8000 now.\n\nsudo firewall-cmd --permanent --add-port 443/tcp\nsudo firewall-cmd --permanent --remove-port=80/tcp\nsudo firewall-cmd --permanent --remove-port=8000/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-ports\n\nNext we restart our JupyterHub service.\n\nsudo systemctl start jupyterhub.service\n\nTry https:\\\\dhub.bluemountain123.live and you should see the JupyterHub login without the http warning." } ] \ No newline at end of file diff --git a/docs/posts/set-up-vm.html b/docs/set-up-vm.html similarity index 66% rename from docs/posts/set-up-vm.html rename to docs/set-up-vm.html index accf160..36f614f 100644 --- a/docs/posts/set-up-vm.html +++ b/docs/set-up-vm.html @@ -2,7 +2,7 @@ - + @@ -24,38 +24,32 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -83,10 +76,10 @@ - - - @@ -97,8 +90,8 @@ +
    @@ -184,10 +177,8 @@

    Set up VM

    - -

    For testing JupyterHub set-ups, I start various Linux machines. Here is how to set up a virtual machines.

    Azure

    @@ -259,33 +250,6 @@

    Azure

    } } } - const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => { - const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light'; - const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark'; - let newTheme = ''; - if(darkModeDefault) { - newTheme = isAlternate ? baseTheme : alternateTheme; - } else { - newTheme = isAlternate ? alternateTheme : baseTheme; - } - const changeGiscusTheme = () => { - // From: https://github.com/giscus/giscus/issues/336 - const sendMessage = (message) => { - const iframe = document.querySelector('iframe.giscus-frame'); - if (!iframe) return; - iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app'); - } - sendMessage({ - setConfig: { - theme: newTheme - } - }); - } - const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null; - if (isGiscussLoaded) { - changeGiscusTheme(); - } - } const toggleColorMode = (alternate) => { // Switch the stylesheets const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate'); @@ -352,15 +316,13 @@

    Azure

    return localAlternateSentinel; } } - const darkModeDefault = false; - let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default'; + let localAlternateSentinel = 'default'; // Dark / light mode switch window.quartoToggleColorScheme = () => { // Read the current dark / light value let toAlternate = !hasAlternateSentinel(); toggleColorMode(toAlternate); setStyleSentinel(toAlternate); - toggleGiscusIfUsed(toAlternate, darkModeDefault); }; // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { @@ -439,9 +401,10 @@

    Azure

    // clear code selection e.clearSelection(); }); - function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { + function tippyHover(el, contentFn) { const config = { allowHTML: true, + content: contentFn, maxWidth: 500, delay: 100, arrow: false, @@ -451,17 +414,8 @@

    Azure

    interactive: true, interactiveBorder: 10, theme: 'quarto', - placement: 'bottom-start', + placement: 'bottom-start' }; - if (contentFn) { - config.content = contentFn; - } - if (onTriggerFn) { - config.onTrigger = onTriggerFn; - } - if (onUntriggerFn) { - config.onUntrigger = onUntriggerFn; - } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); @@ -475,128 +429,6 @@

    Azure

    const note = window.document.getElementById(id); return note.innerHTML; }); - } - const xrefs = window.document.querySelectorAll('a.quarto-xref'); - const processXRef = (id, note) => { - // Strip column container classes - const stripColumnClz = (el) => { - el.classList.remove("page-full", "page-columns"); - if (el.children) { - for (const child of el.children) { - stripColumnClz(child); - } - } - } - stripColumnClz(note) - const typesetMath = (el) => { - if (window.MathJax) { - // MathJax Typeset - window.MathJax.typeset([el]); - } else if (window.katex) { - // KaTeX Render - var mathElements = el.getElementsByClassName("math"); - var macros = []; - for (var i = 0; i < mathElements.length; i++) { - var texText = mathElements[i].firstChild; - if (mathElements[i].tagName == "SPAN") { - window.katex.render(texText.data, mathElements[i], { - displayMode: mathElements[i].classList.contains('display'), - throwOnError: false, - macros: macros, - fleqn: false - }); - } - } - } - } - if (id === null || id.startsWith('sec-')) { - // Special case sections, only their first couple elements - const container = document.createElement("div"); - if (note.children && note.children.length > 2) { - for (let i = 0; i < 2; i++) { - container.appendChild(note.children[i].cloneNode(true)); - } - typesetMath(container); - return container.innerHTML - } else { - typesetMath(note); - return note.innerHTML; - } - } else { - // Remove any anchor links if they are present - const anchorLink = note.querySelector('a.anchorjs-link'); - if (anchorLink) { - anchorLink.remove(); - } - typesetMath(note); - return note.innerHTML; - } - } - for (var i=0; i res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.getElementById(id); - if (note !== null) { - const html = processXRef(id, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - } else { - // See if we can fetch a full url (with no hash to target) - // This is a special case and we should probably do some content thinning / targeting - fetch(url) - .then(res => res.text()) - .then(html => { - const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(html, "text/html"); - const note = htmlDoc.querySelector('main.content'); - if (note !== null) { - // This should only happen for chapter cross references - // (since there is no id in the URL) - // remove the first header - if (note.children.length > 0 && note.children[0].tagName === "HEADER") { - note.children[0].remove(); - } - const html = processXRef(null, note); - instance.setContent(html); - } - }).finally(() => { - instance.enable(); - instance.show(); - }); - } - }, function(instance) { - }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { @@ -639,7 +471,6 @@

    Azure

    } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; - div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); @@ -665,32 +496,6 @@

    Azure

    }); selectedAnnoteEl = undefined; }; - // Handle positioning of the toggle - window.addEventListener( - "resize", - throttle(() => { - elRect = undefined; - if (selectedAnnoteEl) { - selectCodeLines(selectedAnnoteEl); - } - }, 10) - ); - function throttle(fn, ms) { - let throttle = false; - let timer; - return (...args) => { - if(!throttle) { // first call gets through - fn.apply(this, args); - throttle = true; - } else { // all the others get throttled - if(timer) clearTimeout(timer); // cancel #2 - timer = setTimeout(() => { - fn.apply(this, args); - timer = throttle = false; - }, ms); - } - }; - } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { @@ -752,7 +557,7 @@

    Azure