tag:blogger.com,1999:blog-42799714863117437462024-03-15T02:31:47.065+09:00サーバ屋日記Seeking remote work overseas. Fluent in English, Mandarin and Japanese.ktakahttp://www.blogger.com/profile/17802956090186648544noreply@blogger.comBlogger99125tag:blogger.com,1999:blog-4279971486311743746.post-5076208898366468822024-02-03T04:12:00.007+09:002024-02-13T11:32:52.997+09:00Sign in with Google in HTMX+FastAPI<!-- # Sign in with Google in HTMX+FastAPI -->
<ul>
<li><a href="#tldr">TL;DR</a></li>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#what-i-implemented">What I Implemented</a>
<ul>
<li><a href="#anonymous-user-page">Anonymous User Page</a></li>
<li><a href="#authenticated-user-page">Authenticated User Page</a></li>
</ul></li>
<li><a href="#htmx-with-fastapi">HTMX with FastAPI</a></li>
<li><a href="#sign-in">Sign In</a>
<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#frontend-auth">Frontend Auth</a>
<ul>
<li><a href="#sign-in-with-google-button">Sign in with Google
button</a></li>
<li><a href="#the-callback-function">The Callback Function</a></li>
</ul></li>
<li><a href="#backend-auth">Backend Auth</a></li>
</ul></li>
<li><a href="#sign-out">Sign Out</a>
<ul>
<li><a href="#overview-1">Overview</a></li>
<li><a href="#frontend">Frontend</a></li>
<li><a href="#backend">Backend</a></li>
</ul></li>
<li><a href="#switch-navbar">Switch Navbar</a></li>
<li><a href="#protecting-routes">Protecting Routes</a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#seeking-oppotunities">Seeking Oppotunities</a></li>
</ul>
<h1 id="tldr">TL;DR</h1>
<p>The “Sign in with Google” feature has been integrated into a sample
HTMX+FastAPI web application. Either an HTML or JavaScript version of a
code snippet from Google’s code generator is included in the HTML to
display the button. The FastAPI backend has been configured to create a
session from the JWT and set “Set-Cookie: session_id” in the header,
enabling subsequent communications to maintain the login status through
a session cookie. Thanks to HTMX functionality, the application page can
dynamically fetch the navigation bar upon a change in login status,
indicating whether the user is logged in.</p>
<h1 id="introduction">Introduction</h1>
<p>As an aspiring full-stack software developer, I’ve been teaching
myself various front-end web technologies recently. These include
React.js, Svelte, and other shiny new JavaScript frameworks, which can
sometimes be overwhelming. I was considering settling on Svelte, thanks
to its simplicity in state management, when I discovered a concept
called Hypermedia as the Engine of Application State (HATEOAS) and a
library named htmx.js.</p>
<p>Initially, I didn’t quite grasp how it differed from other
technologies. However, after creating several web pages for practice, I
realized I didn’t need to switch into a dedicated front-end programming
mode. I felt liberated from the struggle of forcing myself into the
React mental model, which always seemed nonsensical to me. I often
questioned, ‘Why must I change my mental model every time I create a
small piece of a web page?’</p>
<p>With htmx.js, I can simply create a page, and when I need to change
parts of it, I just fire an AJAX request using hx-get and swap the DOM
elements with HTML fragments in the response. I understand that as a web
application becomes more complex, there might be situations where more
feature-rich technologies are necessary. But now, I’m no longer
intimidated by the overwhelming ecosystems of each technology.</p>
<p>Now, I wanted to explore how I could implement a login feature in a
web application, specifically using the ‘Sign in with Google’ feature on
a web page developed with HTMX and FastAPI. So, I did a bit of research
and figured out how to do it. Noticing a lack of use cases on the web
for this particular combination of technologies, I decided to share my
findings.</p>
<p>While integrating the ‘Sign in with Google’ button into an HTMX page
isn’t much different from incorporating it into a standard HTML page,
the following aspects seemed particularly noteworthy:</p>
<ul>
<li>How to display the status after a successful login or logout using
HTMX.</li>
<li>The handling of the JWT after a successful login on Google’s
side.</li>
<li>How to maintain a session via a cookie generated upon successful JWT
verification, rather than solely relying on the JWT for session
management.</li>
</ul>
<p>These points are especially relevant for beginners like myself.</p>
<h1 id="what-i-implemented">What I Implemented</h1>
<p>The webpage I developed using FastAPI and HTMX is shown in the figure
below. This page integrates a ‘Sign in with Google’ option, enhancing
user experience and offering a secure login method.</p>
<!--
<a href=""
target="_blank">
<img src=""
width="80%" alt="" title="">
</a>
-->
<!--
<a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/"
width="80%" alt="" title="">
</a>
-->
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/FastAPI-HTMX-Google-OAuth043.gif"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/FastAPI-HTMX-Google-OAuth043.gif"
width="80%" alt="Sign-in Animation" title="Sign-in Animation"> </a></p>
<p>The process begins when a user clicks on the Google logo. A pop-up
window appears, guiding them through the Google account selection and
authentication process, which is then executed seamlessly in the
background.</p>
<p>After logging in, the webpage dynamically updates the navigation bar
to include the user’s Google account icon. Additionally, menu elements
like ‘Secret#1’ become accessible, revealing exclusive content with a
single click.</p>
<p>Logging out is just as intuitive. Clicking the ‘Exit’ icon signs the
user out, reverting the navigation bar to its default state for
anonymous visitors.</p>
<p>The source code is available on <a
href="https://github.com/ktaka-ccmp/fastapi-htmx-google-oauth/tree/v1.0.0">my
GitHub repo.</a></p>
<h2 id="anonymous-user-page">Anonymous User Page</h2>
<p>The figure below shows a screenshot of the anonymous user page, which
will be explained in more detail. The page consists of a navigation bar
and a content section. On the navigation bar, an anonymous user icon,
menus including the ones to secret pages, and a Google Sign-in button
are shown. In this example, the ‘Secret#1’ menu is disabled. The
‘Secret#2’ menu is not disabled; however, clicking it will return an
access forbidden error.</p>
<p>The section below the navigation bar is the content section showing
the “Incremental hx-get demo” page, which is accessible by both
anonymous and authenticated users.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/page1.drawio.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/page1.drawio.png"
width="90%" alt="Anonymous User Page" title="Anonymous User Page">
</a></p>
<h2 id="authenticated-user-page">Authenticated User Page</h2>
<p>The figure below shows a screenshot of the authenticated user page,
which also consists of a navigation bar and content section. Shown on
the navigation bar are the user’s Google account icon, menus including
the ones to secret pages, and a Sign-out button. When the user is
authenticated, the menus to the secret pages are both accessible and
return the contents.</p>
<p>The section below the navigation bar is the content section showing
the content of the ‘Secret#1’ page, which is accessible only by
authenticated users.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/page2.drawio.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/page2.drawio.png"
width="90%" alt="Authenticated User Page" title="Authenticated User Page">
</a></p>
<h1 id="htmx-with-fastapi">HTMX with FastAPI</h1>
<p>FastAPI can respond with an HTML page generated from a Jinja
template. The following code specifies that when the FastAPI server
receives a GET request to <code>/spa</code>, it will respond with an
HTML page generated from the Jinja template <code>spa.j2</code>.</p>
<div class="sourceCode" id="cb1"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>router <span class="op">=</span> APIRouter()</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>templates <span class="op">=</span> Jinja2Templates(directory<span class="op">=</span><span class="st">'templates'</span>)</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="at">@router.get</span>(<span class="st">"/spa"</span>, response_class<span class="op">=</span>HTMLResponse)</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> spa(request: Request):</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> context <span class="op">=</span> {<span class="st">"request"</span>: request}</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> templates.TemplateResponse(<span class="st">"spa.j2"</span>, context)</span></code></pre></div>
<p>Shown below is what the template <code>spa.j2</code> looks like.</p>
<pre class="jinja"><code><!DOCTYPE html>
<html lang="en">
{% include 'head.j2' %}
<body>
{# Header #}
<header>
<div id="auth_navbar" hx-get="/auth/auth_navbar" hx-target="#auth_navbar" hx-swap="innerHTML"
hx-trigger="load, LoginStatusChange">
</div>
</header>
{# Content #}
<div class="container" id="content_section" hx-get="/htmx/content.top" hx-target="#content_section" hx-swap="innerHTML"
hx-trigger="load">
</div>
</body>
</html></code></pre>
<p>In the body section, there are two sub-sections: one is wrapped by
<code><header></header></code>, and the other is wrapped by
<code><div></div></code>.</p>
<p>The section wrapped by <code><header></header></code>
loads the navigation bar in a responsive manner. Within this section, we
encounter unfamiliar attributes: hx-get, hx-target, hx-swap, and
hx-trigger. These attributes are interpreted by the HTMX JavaScript
library loaded in <code>head.j2</code>.</p>
<p>The meanings of the attributes are summarized as follows:</p>
<table>
<thead>
<tr class="header">
<th style="text-align: left;">Attribute</th>
<th style="text-align: left;">Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">hx-get</td>
<td style="text-align: left;">issues a GET request to the given URL</td>
</tr>
<tr class="even">
<td style="text-align: left;">hx-target</td>
<td style="text-align: left;">specifies an element for swapping</td>
</tr>
<tr class="odd">
<td style="text-align: left;">hx-swap</td>
<td style="text-align: left;">specifies how content is swapped</td>
</tr>
<tr class="even">
<td style="text-align: left;">hx-trigger</td>
<td style="text-align: left;">specifies the event that triggers the
request</td>
</tr>
</tbody>
</table>
<p><a name="LoginStatusChange"></a> In this case, the HTMX library will
issue a GET request to the <code>/auth/auth_navbar</code> endpoint upon
this section’s first load and when the page receives a response with
“HX-Trigger: LoginStatusChange” in the header for an HTMX AJAX request.
The HTMX library will then replace the content inside the
<code><div></code> section with <code>id="auth_navbar"</code>.</p>
<p>The <code><div></code> section just below the
<code>{# Content #}</code> is there to load the main contents of the
page dynamically. It also has the same set of HTMX attributes summarized
in the table above. In this case, the HTMX library will issue a GET
request to the <code>/htmx/content.top</code> endpoint only upon this
section’s first load. The HTMX library will then replace the content
inside the <code><div></code> section with
<code>id="content_section"</code>.</p>
<p>To have the HTMX attribute properly interpreted, we need to add a
<code><script></code> tag in the document head, like this:</p>
<pre><code><head>
<script defer src="https://unpkg.com/htmx.org@1.9.10"></script>
</head></code></pre>
<p>The document head is included from the file <code>head.j2</code> in
the same directory as:</p>
<pre><code>{% include 'head.j2' %}</code></pre>
<p>These examples illustrate the basic usage of HTMX and FastAPI with
Jinja templating.</p>
<h1 id="sign-in">Sign In</h1>
<h2 id="overview">Overview</h2>
<p>The figure below shows a schematic diagram depicting the flow of the
authentication process.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/htmx-fastapi01.drawio.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/htmx-fastapi01.drawio.png"
width="80%" alt="Sign-in flow" title="Sign-in flow"> </a></p>
<ol type="1">
<li>When a user clicks ‘Sign in with Google’, an authentication request
is sent to Google.</li>
<li>Upon successful authentication, the user’s credentials are returned
as a JSON Web Token (JWT) to the page.</li>
<li>JavaScript code on the page forwards the JWT to
<code>/auth/login</code>, an authentication endpoint prepared using
FastAPI.</li>
<li>The JWT is then verified using a certificate fetched from
Google.</li>
<li>A user corresponding to the JWT is created in the SQLite database,
if one does not already exist.</li>
<li>A new session is also created and stored in the database.</li>
<li>FastAPI sends a response with a header containing the entry
“Set-Cookie: session_id=xxxxxx.”</li>
</ol>
<p>Thereafter, “Cookie: session_id=xxxxxx” is always set in subsequent
communications, until the cookie expires or until the user explicitly
hits the logout button on the web page.</p>
<h2 id="frontend-auth">Frontend Auth</h2>
<h3 id="sign-in-with-google-button">Sign in with Google button</h3>
<p>To display a <code>Sign in with Google</code> button, we need to use
a JavaScript library provided by Google and place a code snippet
somewhere in the HTML.</p>
<p>To load the JavaScript library, add the following
<code><script></code> tag in the document head:</p>
<pre><code><head>
<script src="https://accounts.google.com/gsi/client" async></script>
</head></code></pre>
<p>Inside the navigation bar, we place a code snippet to show the “Sign
in with Google” button. This code snippet is available in two versions:
JavaScript and HTML. Either version can be used.</p>
<h4 id="javascript-version">JavaScript Version</h4>
<div class="sourceCode" id="cb6"><pre
class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="op"><</span>script<span class="op">></span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a> google<span class="op">.</span><span class="at">accounts</span><span class="op">.</span><span class="at">id</span><span class="op">.</span><span class="fu">initialize</span>({</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">client_id</span><span class="op">:</span> <span class="st">"{{ client_id }}"</span><span class="op">,</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">callback</span><span class="op">:</span> onSignIn<span class="op">,</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">ux_mode</span><span class="op">:</span> <span class="st">"popup"</span><span class="op">,</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a> google<span class="op">.</span><span class="at">accounts</span><span class="op">.</span><span class="at">id</span><span class="op">.</span><span class="fu">renderButton</span>(<span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">"signInDiv"</span>)<span class="op">,</span> {</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a> <span class="dt">theme</span><span class="op">:</span> <span class="st">"outline"</span><span class="op">,</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a> <span class="dt">size</span><span class="op">:</span> <span class="st">"large"</span><span class="op">,</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a> <span class="dt">shape</span><span class="op">:</span> <span class="st">"circle"</span><span class="op">,</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a> <span class="dt">type</span><span class="op">:</span> <span class="st">"icon"</span><span class="op">,</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a> {# google<span class="op">.</span><span class="at">accounts</span><span class="op">.</span><span class="at">id</span><span class="op">.</span><span class="fu">prompt</span>()<span class="op">;</span> #}</span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="op"></</span>script<span class="op">></span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a><span class="op"><</span>div id<span class="op">=</span><span class="st">"signInDiv"</span><span class="op">></</span>div<span class="op">></span></span></code></pre></div>
<p>The <code>google.accounts.id.initialize</code> function defines the
initialization and behavior of the sign-in process:</p>
<ul>
<li>The <code>client_id</code> specifies the <a
href="https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid">OAuth
2.0 Client ID</a>, which is necessary.</li>
<li>The <code>callback</code> defines a JavaScript callback function for
a successful sign-in on Google’s side.</li>
<li>The <code>ux_mode</code> sets Google’s sign-in page mode, preferred
to be ‘popup’ instead of ‘redirect’.</li>
</ul>
<p>The <code>google.accounts.id.renderButton</code> function defines the
presentation style of the Sign in with Google button:</p>
<p>The <code>google.accounts.id.prompt</code> method displays the One
Tap prompt (and is disabled in this particular case).</p>
<h4 id="html-version">HTML Version</h4>
<div class="sourceCode" id="cb7"><pre
class="sourceCode html"><code class="sourceCode html"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw"><script</span> <span class="er">src</span><span class="ot">=</span><span class="st">"https://accounts.google.com/gsi/client"</span> <span class="er">async</span><span class="kw">></script></span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="kw"><div</span> <span class="er">id</span><span class="ot">=</span><span class="st">"g_id_onload"</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-client_id=</span><span class="st">"{{ client_id }}"</span><span class="er">,</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-context=</span><span class="st">"signin"</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-ux_mode=</span><span class="st">"popup"</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-callback=</span><span class="st">onSignIn</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-close_on_tap_outside=</span><span class="st">"true"</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-itp_support=</span><span class="st">"true"</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-auto_prompt=</span><span class="st">"false"</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">></span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="kw"></div></span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="kw"><div</span> <span class="er">class</span><span class="ot">=</span><span class="st">"g_id_signin"</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-type=</span><span class="st">"icon"</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-shape=</span><span class="st">"square"</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-theme=</span><span class="st">"outline"</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a><span class="ot"> data-size=</span><span class="st">"large"</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">></span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a><span class="kw"></div></span></span></code></pre></div>
<p>The <code><div id="g_id_onload"></code> element initializes and
configures the sign-in process:</p>
<ul>
<li><code>data-client_id="{{ client_id }}"</code> specifies the
necessary <a
href="https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid">OAuth
2.0 Client ID</a>.</li>
<li><code>data-ux_mode="popup"</code> sets the mode of Google’s sign-in
page to ‘popup’ instead of the more intrusive ‘redirect’.</li>
<li><code>data-callback=onSignIn</code> specifies a JavaScript callback
function for a successful sign-in on Google’s side.</li>
<li>Setting <code>data-auto_prompt="false"</code> determines whether to
display One Tap or not.</li>
</ul>
<p>The <code><div id="g_id_sigin"></code> division defines the
presentation style of the Sign in with Google button.</p>
<h3 id="the-callback-function">The Callback Function</h3>
<p>Below is an implementation of the callback function to forward the
JWT to the backend:</p>
<div class="sourceCode" id="cb8"><pre
class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="op"><</span>script<span class="op">></span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">function</span> <span class="fu">onSignIn</span>(response) {</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a> htmx<span class="op">.</span><span class="fu">ajax</span>(<span class="st">'POST'</span><span class="op">,</span> <span class="st">'{{ login_url }}'</span><span class="op">,</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a> { <span class="dt">values</span><span class="op">:</span> { <span class="st">'credential'</span><span class="op">:</span> response<span class="op">.</span><span class="at">credential</span> }<span class="op">,</span> <span class="dt">swap</span><span class="op">:</span> <span class="st">'none'</span> })</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="op"></</span>script<span class="op">></span></span></code></pre></div>
<p>The <code>onSignIn</code> function sends the JWT received from
Google’s sign-in page to <code>{{ login_url }}</code>, a backend
endpoint designed to handle the received JWT.</p>
<h2 id="backend-auth">Backend Auth</h2>
<p>The backend endpoint receives the JWT, verifies it using Google’s
public certificate, and then creates a session to maintain a logged-in
status in subsequent communications.</p>
<p>Here is the code snippet of the backend endpoint, which performs the
following operations:</p>
<ul>
<li><code>VerifyToken</code>: Verifies the JWT.</li>
<li><code>GetOrCreateUser</code>: Creates the user in the database if
they don’t already exist.</li>
<li><code>create_session</code>: Creates a session and stores it in a
session database.</li>
<li><code>response.set_cookie</code>: Sets the session_id in the
cookie.</li>
<li>Returns the response to the frontend.</li>
</ul>
<div class="sourceCode" id="cb9"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="at">@router.post</span>(<span class="st">"/login"</span>)</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> login(request: Request, ds: Session <span class="op">=</span> Depends(get_db), cs: Session <span class="op">=</span> Depends(get_cache)):</span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> body <span class="op">=</span> <span class="cf">await</span> request.body()</span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> jwt <span class="op">=</span> <span class="bu">dict</span>(urllib.parse.parse_qsl(body.decode(<span class="st">'utf-8'</span>))).get(<span class="st">'credential'</span>)</span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> idinfo <span class="op">=</span> <span class="cf">await</span> VerifyToken(jwt)</span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> idinfo:</span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Error: Failed to validate JWT token"</span>)</span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Response(<span class="st">"Error: Failed to validate JWT token"</span>)</span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a> user <span class="op">=</span> <span class="cf">await</span> GetOrCreateUser(idinfo, ds)</span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> user:</span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Error: Failed to GetOrCreateUser"</span>)</span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Response(<span class="st">"Error: Failed to GetOrCreateUser for the JWT"</span>)</span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a> session_id <span class="op">=</span> create_session(user, cs)</span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> session_id:</span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Error: Failed to create session for"</span>, user.name)</span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Response(<span class="st">"Error: Failed to create session for"</span><span class="op">+</span>user.name)</span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a> max_age <span class="op">=</span> <span class="dv">600</span></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a> expires <span class="op">=</span> datetime.now(timezone.utc) <span class="op">+</span> timedelta(seconds<span class="op">=</span>max_age)</span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a> response <span class="op">=</span> JSONResponse({<span class="st">"Authenticated_as"</span>: user.name})</span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a> response.set_cookie(</span>
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a> key<span class="op">=</span><span class="st">"session_id"</span>,</span>
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a> value<span class="op">=</span>session_id,</span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a> httponly<span class="op">=</span><span class="va">True</span>,</span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a> samesite<span class="op">=</span><span class="st">"lax"</span>,</span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a> max_age<span class="op">=</span>max_age,</span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a> expires<span class="op">=</span>expires,</span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a> response.headers[<span class="st">"HX-Trigger"</span>] <span class="op">=</span> <span class="st">"LoginStatusChange"</span></span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> response</span></code></pre></div>
<p>Please note that there is a line setting “HX-Trigger:
LoginStatusChange” in the response header. This triggers an hx-get to
<code>/auth/auth_navbar</code>, causing the navigation bar to reload <a
href="#LoginStatusChange">(see above)</a>.</p>
<p>The VerifyToken function below verifies the JWT from the frontend
using the <a
href="https://google-auth.readthedocs.io/en/stable/reference/google.oauth2.id_token.html">Google
OAuth2 Python library</a>.</p>
<div class="sourceCode" id="cb10"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> google.oauth2 <span class="im">import</span> id_token</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> google.auth.transport <span class="im">import</span> requests</span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> VerifyToken(jwt: <span class="bu">str</span>):</span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> <span class="cf">try</span>:</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> idinfo <span class="op">=</span> id_token.verify_oauth2_token(</span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> jwt,</span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> requests.Request(),</span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> settings.google_oauth2_client_id)</span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">except</span> <span class="pp">ValueError</span>:</span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Error: Failed to validate JWT token with GOOGLE_OAUTH2_CLIENT_ID="</span> <span class="op">+</span> settings.google_oauth2_client_id <span class="op">+</span><span class="st">"."</span>)</span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"idinfo: "</span>, idinfo)</span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> idinfo</span></code></pre></div>
<h1 id="sign-out">Sign Out</h1>
<h2 id="overview-1">Overview</h2>
<p>The figure below shows a schematic diagram depicting the flow of the
sign out process.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/htmx-fastapi02.drawio.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/ktaka.blog.ccmp.jp/master/2024/HTMX-FasAPI-Sign-in-with-Google/image/htmx-fastapi02.drawio.png"
width="80%" alt="Sign-in flow" title="Sign-in flow"> </a></p>
<ol type="1">
<li>When a user clicks the ‘Sign-out’ button, an hx-get request is sent
to /auth/logout, an endpoint prepared using FastAPI.</li>
<li>The session associated with the session_id is deleted in the
database.</li>
<li>FastAPI sends a response with a header containing the entry
<code>Set-Cookie: session_id="", max_age=0</code>.</li>
</ol>
<p>Thereafter, the browser unsets the <code>session_id</code> in the
Cookie header and will no longer send the <code>session_id</code> cookie
in subsequent requests.</p>
<h2 id="frontend">Frontend</h2>
<p>Here is a snippet of the Jinja template, which shows a logout button
for authenticated users in the navigation bar.</p>
<div class="sourceCode" id="cb11"><pre
class="sourceCode html"><code class="sourceCode html"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a> <span class="kw"><ul</span> <span class="er">class</span><span class="ot">=</span><span class="st">"navbar-nav me-auto mb-2 mb-lg-0"</span><span class="kw">></span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="kw"><div</span> <span class="er">hx-get</span><span class="ot">=</span><span class="st">"{{logout_url}}"</span> <span class="er">hx-trigger</span><span class="ot">=</span><span class="st">"click"</span> <span class="er">hx-target</span><span class="ot">=</span><span class="st">"#content_section"</span> <span class="er">hx-swap</span><span class="ot">=</span><span class="st">"innerHTML"</span><span class="kw">></span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a> <span class="kw"><img</span> <span class="er">class</span><span class="ot">=</span><span class="st">"rounded-circle"</span> <span class="er">src</span><span class="ot">=</span><span class="st">"{{icon_url}}"</span> <span class="er">alt</span><span class="ot">=</span><span class="st">"logout"</span> <span class="er">name</span><span class="ot">=</span><span class="st">"logout"</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="ot"> style=</span><span class="st">"width:2rem;height:2rem;border-radius:2rem"</span><span class="kw">></span></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a> <span class="kw"></div></span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a> <span class="kw"></ul></span></span></code></pre></div>
<p>The <code>{{ logout_url }}</code> and <code>{{icon_url}}</code> will
be filled by the <a href="#auth_navbar">(backend code)</a> when it’s
returned to the client browsers.</p>
<p>Upon a click, the logout button fires an hx-get request to
<code>{{ logout_url }}</code>, which translates to
<code>/auth/logout</code>, an endpoint of the backend server. The
attributes <code>hx-target="#content_section"</code> and
<code>hx-swap="innerHTML"</code> will cause the HTMX libray to replace
the content in inside the
<code><div id="content_section"></div></code>.</p>
<h2 id="backend">Backend</h2>
<p>The backend endpoint receives an AJAX get request, deletes the
session associated with the session_id from the database, and instructs
the browser to unset the ‘Cookie: session_id’.</p>
<p>Here is the code snippet of the backend endpoint, which performs the
following operations:</p>
<ul>
<li><code>delete_session</code>: delete the session from the session
database.</li>
<li><code>response.delete_cookie</code>: Causes the browser to delete
the <code>session_id</code> cookie from the Cookie header by setting
<code>Set-Cookie: session_id="", max-age=0</code> in the response
header(see <a
href="https://fastapi.tiangolo.com/reference/response/#fastapi.Response.delete_cookie">explanation</a>
and <a
href="https://github.com/encode/starlette/blob/master/starlette/responses.py#L130">source
code</a>).</li>
</ul>
<div class="sourceCode" id="cb12"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="at">@router.get</span>(<span class="st">"/logout"</span>, response_class<span class="op">=</span>HTMLResponse)</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> logout(request: Request, response: Response, hx_request: Optional[<span class="bu">str</span>] <span class="op">=</span> Header(<span class="va">None</span>), cs: Session <span class="op">=</span> Depends(get_cache)):</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> hx_request:</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> HTTPException(</span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a> status_code<span class="op">=</span>status.HTTP_400_BAD_REQUEST,</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a> detail<span class="op">=</span><span class="st">"Only HX request is allowed to this end point."</span></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a> req_session_id <span class="op">=</span> request.cookies.get(<span class="st">"session_id"</span>) <span class="co"># get session_id from cookie of request</span></span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> req_session_id:</span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a> delete_session(req_session_id, cs)</span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a> response.delete_cookie(<span class="st">"session_id"</span>) <span class="co"># delete key="session_id" from cookie of response</span></span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a> context <span class="op">=</span> {<span class="st">"request"</span>: request, <span class="st">"message"</span>: <span class="st">"User logged out"</span>}</span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a> response <span class="op">=</span> templates.TemplateResponse(<span class="st">"content.error.j2"</span>, context)</span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a> response.headers[<span class="st">"HX-Trigger"</span>] <span class="op">=</span> <span class="st">"LoginStatusChange"</span></span>
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> response</span></code></pre></div>
<p>Please also note that there is a line setting “HX-Trigger:
LoginStatusChange” in the response header as same as the case for
/auth/login endpoint. This triggers an hx-get to
<code>/auth/auth_navbar</code>, causing the navigation bar to reload <a
href="#LoginStatusChange">(see above)</a>.</p>
<h1 id="switch-navbar">Switch Navbar</h1>
<p>The following code snippet returns the partial HTML content to
display the navigation bar. Depending on the existence of the valid
session_id in the Cookie header, the code returns the different HTMLs.
When there is a valid session_id in the Cookie header, it returns the
HTML generated from a Jinja template <code>auth_navbar.logout.j2</code>
for authenticated users with the user’s Google account icon and logout
button. When there isn’t a valid session_id in the Cookie header, it
returns the HTML generated from a Jinja template
<code>auth_navbar.login.j2</code> for anonymous users with a “Sign in
with Google” button.</p>
<p><a name="auth_navbar"></a></p>
<div class="sourceCode" id="cb13"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="at">@router.get</span>(<span class="st">"/auth_navbar"</span>, response_class<span class="op">=</span>HTMLResponse)</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> auth_navbar(request: Request, hx_request: Optional[<span class="bu">str</span>] <span class="op">=</span> Header(<span class="va">None</span>), ds: Session <span class="op">=</span> Depends(get_db), cs: Session <span class="op">=</span> Depends(get_cache)):</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> hx_request:</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> HTTPException(</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> status_code<span class="op">=</span>status.HTTP_400_BAD_REQUEST,</span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> detail<span class="op">=</span><span class="st">"Only HX request is allowed to this end point."</span></span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a> <span class="co"># For authenticated users, return the menu.logout component.</span></span>
<span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">try</span>:</span>
<span id="cb13-12"><a href="#cb13-12" aria-hidden="true" tabindex="-1"></a> session_id <span class="op">=</span> request.cookies.get(<span class="st">"session_id"</span>)</span>
<span id="cb13-13"><a href="#cb13-13" aria-hidden="true" tabindex="-1"></a> user <span class="op">=</span> <span class="cf">await</span> get_current_user(session_id<span class="op">=</span>session_id, cs<span class="op">=</span>cs, ds<span class="op">=</span>ds)</span>
<span id="cb13-14"><a href="#cb13-14" aria-hidden="true" tabindex="-1"></a> logout_url <span class="op">=</span> settings.origin_server <span class="op">+</span> <span class="st">"/auth/logout"</span></span>
<span id="cb13-15"><a href="#cb13-15" aria-hidden="true" tabindex="-1"></a> icon_url <span class="op">=</span> settings.origin_server <span class="op">+</span> <span class="st">"/img/logout.png"</span></span>
<span id="cb13-16"><a href="#cb13-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-17"><a href="#cb13-17" aria-hidden="true" tabindex="-1"></a> context <span class="op">=</span> {<span class="st">"request"</span>: request, <span class="st">"session_id"</span>: session_id, <span class="st">"logout_url"</span>:logout_url, <span class="st">"icon_url"</span>: icon_url,</span>
<span id="cb13-18"><a href="#cb13-18" aria-hidden="true" tabindex="-1"></a> <span class="st">"name"</span>: user.name, <span class="st">"picture"</span>: user.picture, <span class="st">"email"</span>: user.email}</span>
<span id="cb13-19"><a href="#cb13-19" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> templates.TemplateResponse(<span class="st">"auth_navbar.logout.j2"</span>, context)</span>
<span id="cb13-20"><a href="#cb13-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">except</span>:</span>
<span id="cb13-21"><a href="#cb13-21" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"User not logged-in."</span>)</span>
<span id="cb13-22"><a href="#cb13-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-23"><a href="#cb13-23" aria-hidden="true" tabindex="-1"></a> <span class="co"># For unauthenticated users, return the menu.login component.</span></span>
<span id="cb13-24"><a href="#cb13-24" aria-hidden="true" tabindex="-1"></a> client_id <span class="op">=</span> settings.google_oauth2_client_id</span>
<span id="cb13-25"><a href="#cb13-25" aria-hidden="true" tabindex="-1"></a> login_url <span class="op">=</span> settings.origin_server <span class="op">+</span> <span class="st">"/auth/login"</span></span>
<span id="cb13-26"><a href="#cb13-26" aria-hidden="true" tabindex="-1"></a> icon_url <span class="op">=</span> settings.origin_server <span class="op">+</span> <span class="st">"/img/icon.png"</span></span>
<span id="cb13-27"><a href="#cb13-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-28"><a href="#cb13-28" aria-hidden="true" tabindex="-1"></a> context <span class="op">=</span> {<span class="st">"request"</span>: request, <span class="st">"client_id"</span>: client_id, <span class="st">"login_url"</span>: login_url, <span class="st">"icon_url"</span>: icon_url}</span>
<span id="cb13-29"><a href="#cb13-29" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> templates.TemplateResponse(<span class="st">"auth_navbar.login.j2"</span>, context)</span></code></pre></div>
<p>The <code>settings.origin_server</code> and
<code>settings.google_oauth2_client_id</code> are defined else where in
the code tree to provied necessary information from a <code>.env</code>
file.</p>
<h1 id="protecting-routes">Protecting Routes</h1>
<p>Access to the secret pages is controlled through the
<code>auth.is_authenticated</code> dependency, as shown in the following
code snippet. If the <code>auth.is_authenticated</code> does not raise
an exception, it allows access to the routes defined in
<code>htmx/htmx_secret.py</code>; otherwise, it disallows it.</p>
<div class="sourceCode" id="cb14"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> htmx <span class="im">import</span> htmx_secret</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a>app <span class="op">=</span> FastAPI()</span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>app.include_router(</span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a> htmx_secret.router,</span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a> prefix<span class="op">=</span><span class="st">"/htmx"</span>,</span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a> tags<span class="op">=</span>[<span class="st">"htmx"</span>],</span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a> dependencies<span class="op">=</span>[Depends(auth.is_authenticated)],</span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
<p>The <code>auth.is_authenticated</code> is defined as the
following:</p>
<ul>
<li>In the function <code>get_current_user</code>, the user’s email is
retrieved from the session database, and then the other information is
retrieved from the user database.</li>
<li>The function <code>is_authenticated</code> raises an HTTPException
when there is no user information returned by the
<code>get_current_user</code> or the <code>user.disabled</code> is
true.</li>
</ul>
<div class="sourceCode" id="cb15"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> get_current_user(session_id: <span class="bu">str</span>, ds: Session <span class="op">=</span> Depends(get_db), cs: Session <span class="op">=</span> Depends(get_cache)):</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> session_id:</span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a> session <span class="op">=</span> get_session_by_session_id(session_id, cs)</span>
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> session:</span>
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a> user_dict <span class="op">=</span> get_user_by_email(session[<span class="st">"email"</span>], ds)</span>
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a> user<span class="op">=</span>UserBase(<span class="op">**</span>user_dict)</span>
<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-12"><a href="#cb15-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> user</span>
<span id="cb15-13"><a href="#cb15-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-14"><a href="#cb15-14" aria-hidden="true" tabindex="-1"></a><span class="at">@router.get</span>(<span class="st">"/is_authenticated"</span>)</span>
<span id="cb15-15"><a href="#cb15-15" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> is_authenticated(session_id: Annotated[<span class="bu">str</span> <span class="op">|</span> <span class="va">None</span>, Cookie()] <span class="op">=</span> <span class="va">None</span>, ds: Session <span class="op">=</span> Depends(get_db), cs: Session <span class="op">=</span> Depends(get_cache)):</span>
<span id="cb15-16"><a href="#cb15-16" aria-hidden="true" tabindex="-1"></a> user <span class="op">=</span> <span class="cf">await</span> get_current_user(session_id<span class="op">=</span>session_id, cs<span class="op">=</span>cs, ds<span class="op">=</span>ds)</span>
<span id="cb15-17"><a href="#cb15-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-18"><a href="#cb15-18" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> user:</span>
<span id="cb15-19"><a href="#cb15-19" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> HTTPException(</span>
<span id="cb15-20"><a href="#cb15-20" aria-hidden="true" tabindex="-1"></a> status_code<span class="op">=</span>status.HTTP_401_UNAUTHORIZED,</span>
<span id="cb15-21"><a href="#cb15-21" aria-hidden="true" tabindex="-1"></a> detail<span class="op">=</span><span class="st">"NotAuthenticated"</span></span>
<span id="cb15-22"><a href="#cb15-22" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb15-23"><a href="#cb15-23" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> user.disabled:</span>
<span id="cb15-24"><a href="#cb15-24" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> HTTPException(</span>
<span id="cb15-25"><a href="#cb15-25" aria-hidden="true" tabindex="-1"></a> status_code<span class="op">=</span>status.HTTP_403_FORBIDDEN,</span>
<span id="cb15-26"><a href="#cb15-26" aria-hidden="true" tabindex="-1"></a> detail<span class="op">=</span><span class="st">"Disabled user"</span></span>
<span id="cb15-27"><a href="#cb15-27" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb15-28"><a href="#cb15-28" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb15-29"><a href="#cb15-29" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Authenticated."</span>)</span>
<span id="cb15-30"><a href="#cb15-30" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> JSONResponse({<span class="st">"message"</span>: <span class="st">"Authenticated"</span>})</span></code></pre></div>
<h1 id="conclusion">Conclusion</h1>
<p>In this post, I’ve shared what I learned about integrating the Sign
in with Google feature with an HTMX+FastAPI web application. The “Sign
in with Google” button has been successfully integrated into a sample
HTMX+FastAPI web application. We included either an HTML or JavaScript
version of a code snippet from Google’s code generator in the HTML to
display the button. The FastAPI backend has been configured to create a
session from the JWT and set “Set-Cookie: session_id” in the header,
allowing subsequent communications to maintain the login status through
a session cookie. Thanks to HTMX functionality, the application page can
dynamically update the navigation bar upon a change in login status,
clearly indicating whether the user is logged in.</p>
<h1 id="seeking-oppotunities">Seeking Oppotunities</h1>
<p>I’m based in Japan and am seeking remote work opportunities overseas.
I’m also open to on-site positions in North America, Australia, the EU,
etc., if visa support is available. <a
href="https://www.linkedin.com/in/ktaka-phd/">LinkedIn</a></p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-7228092507267801652024-01-10T00:50:00.003+09:002024-01-11T02:49:02.954+09:00Google OAuth2 in Svelte + FastAPI: Create a Session Cookie from JWT<h1 id="table-of-contents">Table of Contents</h1>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#what-i-implement">What I Implement</a></li>
<li><a href="#frontend-implementation-with-svelte">Frontend
implementation with Svelte</a></li>
<li><a href="#backend-implementation-with-fastapi">Backend
implementation with FastAPI</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="introduction">Introduction</h2>
<p>I have implemented Google Sign-In functionality in a sample website
built using Svelte and FastAPI. There are various methods to
authenticate users in the backend API server after a successful Google
Sign In. One common approach is to send the JWT received from Google in
the request header as <code>Authorization: "Bearer: JWT",</code> and if
the JWT is valid, authorization to access resources is granted. Another
typical method involves issuing a JWT on the backend and using it for
user authentication in the <code>Authorization</code> header. However,
using JWT directly for session management poses a challenge in immediate
invalidation if the JWT is leaked. Reference: <a
href="http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/">Stop
using JWT for sessions</a> . Therefore, I implemented a method in which,
following the receipt of the JWT from Google, FastAPI assigns a new
session_id. This session_id is set in a cookie to maintain the
session.</p>
<p>The session information is managed in FastAPI’s session database,
allowing administrators to invalidate sessions anytime. Additionally, by
adding Secure and HttpOnly attributes to the cookies, interception
during transmission and access from JavaScript are prevented, enabling
the development of a more secure website.</p>
<p>Note: I am self-taught in both Svelte and FastAPI, so I would
appreciate any advice on improving anything.</p>
<h2 id="what-i-implement">What I Implement</h2>
<p>With authentication implemented, unauthenticated access will redirect
to the login page, where you can log in with a Google account.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthLogin3-2.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthLogin3-2.png"
width="80%" alt="Login page" title="Login page"> </a></p>
<p>The Customer page can only be displayed after successful
authentication.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthCustomer.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthCustomer.png"
width="80%" alt="Customer page for authenticated users" title="Customer page for authenticated users">
</a></p>
<p>In FastAPI, the Swagger UI automatically generates a documentation
page.</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/fastapi01.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/fastapi01.png"
width="80%" alt="FastAPI OpenAPI doc page" title="FastAPI OpenAPI doc page">
</a></p>
<h2 id="frontend-implementation-with-svelte">Frontend implementation
with Svelte</h2>
<p>I implemented the frontend JavaScript application using Svelte. It
includes authentication functionality using Google OAuth2 and retrieves
customer data from the backend to display in a table.</p>
<p>Upon successful Google Sign-In, the obtained JWT is sent to the
backend API server. The backend verifies the JWT, creates a user
account, sets the session_id in a cookie, and returns a response.
Thereafter, the session_id is always sent in the cookie to maintain a
session.</p>
<p>The code for this implementation is available in the following
repository:</p>
<ul>
<li><a
href="https://github.com/ktaka-ccmp/google-oauth2-example/tree/v2.1.1/google-oauth/frontend-svelte">frontend-svelte
code</a></li>
</ul>
<p>I will explain the key points of implementing the login functionality
below.</p>
<h3 id="routing">Routing</h3>
<p>We use <code>svelete-routing</code> to set up routing as follows:</p>
<ul>
<li><strong>/customer</strong> : Displays the Customer component.</li>
<li><strong>/login</strong> : Displays the LoginPage component.</li>
</ul>
<p>Sample code for <code>App.svelte</code> is as follows:</p>
<pre class="svelte"><code><script>
import { Router, Link, Route } from "svelte-routing";
import Top from "./components/Top.svelte";
import Customer from "./components/Customer.svelte";
import NoMatch from "./components/NoMatch.svelte";
import LoginPage from "./components/LoginPage.svelte";
export let url = "";
</script>
<div class="container-sm">
<Router {url}>
<nav>
<table class="table-borderless table-responsive">
<tbody>
<tr><td><Link to="/">Top</Link></td></tr>
<tr><td><Link to="/customer">Customer</Link></td></tr>
</tbody>
</table>
</nav>
<div>
<Route path="/"><Top /></Route>
<Route path="/customer"><Customer /></Route>
<Route path="/login"><LoginPage /></Route>
<Route path="*"><NoMatch /></Route>
</div>
</Router>
</div></code></pre>
<h3 id="login-page">Login Page</h3>
<p>We display Google’s Sign-In button and also use the OneTap interface.
After signing in with Google, the callback function
<code>backendAuth</code> is called. <code>backendAuth</code> sends the
response obtained from Google Sign-In to
<code>http://localhost/api/login</code>. The response includes the JWT
token. If the backend login is successful, it redirects to the previous
page. If it fails, the error is handled and “navigated” back to the
login page.</p>
<p>Sample code for <code>LoginPage.svelte</code> is as follows:</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios";
import { useLocation, navigate } from "svelte-routing";
import { jwtDecode } from "jwt-decode";
let location = useLocation();
let origin = $location.state?.from;
const backendAuth = (response) => {
const data = JSON.stringify(response, null, 2);
console.log("JWT fed to backendAuth:\n", data);
apiAxios
.post(`/api/login/`, data)
.then((res) => {
console.log("Navigate back to: ", origin);
navigate(origin, { replace: true });
})
.catch((error) => {
console.log("backendAuth failed. Redirecting to /login... ");
});
};
const onLogin = backendAuth;
onMount(() => {
google.accounts.id.initialize({
/* global google */
client_id: import.meta.env.VITE_APP_GOOGLE_OAUTH2_CLIENT_ID,
callback: (r) => onLogin(r),
ux_mode: "popup",
// ux_mode: "redirect",
});
google.accounts.id.renderButton(document.getElementById("signInDiv"), {
theme: "filled_blue",
size: "large",
shape: "circle",
});
google.accounts.id.prompt();
});
</script>
<main>
<h2>Login page</h2>
<div id="signInDiv"></div>
</main></code></pre>
<h3 id="setup-of-axios-instance">Setup of Axios Instance</h3>
<p>By setting <code>withCredentials: true</code>, axios will send
cookies. Axios’s interceptors are used for error handling. If
<code>401 Unauthorized</code> or <code>403 Forbidden</code> are returned
from the backend, it navigates to <code>/login</code>.</p>
<p>Sample code for <code>apiAxios.js</code> is as follows:</p>
<div class="sourceCode" id="cb3"><pre
class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> axios <span class="im">from</span> <span class="st">"axios"</span><span class="op">;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> { navigate } <span class="im">from</span> <span class="st">"svelte-routing"</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="im">export</span> <span class="kw">const</span> apiAxios <span class="op">=</span> axios<span class="op">.</span><span class="fu">create</span>({</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">baseURL</span><span class="op">:</span> <span class="vs">`</span><span class="sc">${</span><span class="im">import</span><span class="op">.</span><span class="at">meta</span><span class="op">.</span><span class="at">env</span><span class="op">.</span><span class="at">VITE_APP_API_SERVER</span><span class="sc">}</span><span class="vs">`</span><span class="op">,</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> <span class="dt">withCredentials</span><span class="op">:</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>apiAxios<span class="op">.</span><span class="at">interceptors</span><span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="fu">use</span>(</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> (response) <span class="kw">=></span> {</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> response<span class="op">;</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> (error) <span class="kw">=></span> {</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (error<span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="at">status</span> <span class="op">===</span> <span class="dv">401</span> <span class="op">||</span> error<span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="at">status</span> <span class="op">===</span> <span class="dv">403</span>) {</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="st">"apiAxios failed. Redirecting to /login... from"</span><span class="op">,</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> location<span class="op">.</span><span class="at">pathname</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="fu">navigate</span>(<span class="st">"/login"</span><span class="op">,</span> { <span class="dt">state</span><span class="op">:</span> { <span class="dt">from</span><span class="op">:</span> location<span class="op">.</span><span class="at">pathname</span> }<span class="op">,</span> <span class="dt">replace</span><span class="op">:</span> <span class="kw">true</span> })<span class="op">;</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">Promise</span><span class="op">.</span><span class="fu">reject</span>(error)<span class="op">;</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></code></pre></div>
<h3 id="logoutbutton-component">LogoutButton Component</h3>
<p>This component displays a Logout button. On mount, it accesses the
backend server to get information about the logged-in user. If there is
no session_id in the cookie, meaning the user is not logged in, the
attempt to get user information fails, and the user is redirected to the
<code>/login</code> page due to the error handling in
<code>apiAxios.interceptor</code>.</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios.js";
let user;
onMount(() => {
console.log("Logout Component Mounted");
getUser();
});
const handleLogout = () => {
user = null;
apiAxios
.get(`/api/logout/`)
.then((res) => {
console.log("backendLogout", res);
getUser();
})
.catch((error) => console.log("Logout failed: ", error));
};
const getUser = () => {
apiAxios
.get(`/api/user/`)
.then((res) => {
user = res.data;
console.log("getUser: user:", user);
})
.catch((error) => console.log("getUser failed: ", error.response));
};
const onLogout = handleLogout;
</script>
<div>
Authenticated as {user?.username} &nbsp;
<button type="button" on:click={onLogout}>Sign Out</button>
</div></code></pre>
<h3 id="customer-component">Customer Component</h3>
<p>This component retrieves data from the backend server and displays it
in a table. Since the <code>LogoutButton</code> component is placed on
the page, if the user is not logged in, it redirects to the
<code>/login</code> page.</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios";
import LogoutButton from "./LogoutButton.svelte";
let customers = [];
let Loading = true;
onMount(async () => {
await new Promise((r) => setTimeout(r, 1000));
apiAxios
.get(`/api/customer/`)
.then((res) => (customers = res.data.results))
.catch((error) => console.log(error))
.finally(() => Loading = false);
});
</script>
<LogoutButton />
<h2>This is Customer.</h2>
{#if Loading}
<p>Loading ...</p>
{:else}
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead class="table-light">
<tr>
<th>id</th>
<th>name</th>
<th>email</th>
</tr>
</thead>
<tbody>
{#each customers as cs}
<tr>
<td>{cs.id}</td>
<td>{cs.name}</td>
<td>{cs.email}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}</code></pre>
<h2 id="backend-implementation-with-fastapi">Backend implementation with
FastAPI</h2>
<p>I implemented the backend API server using FastAPI. It verifies the
JWT received from the frontend Javascript apps, creates a user account,
issues a session_id, and registers it in the session database. The
created session_id is set in a cookie and returned in the response. The
backend API server creates a new user if a user corresponding to the JWT
does not exist in the database.</p>
<p>When a request to an endpoint that requires authentication is
received, FastAPI checks the session_id set in the cookie against the
session database and returns the requested data if valid session
information exists.</p>
<p>The code for this implementation is available in the following
repository:</p>
<ul>
<li><a
href="https://github.com/ktaka-ccmp/google-oauth2-example/tree/v2.1.1/google-oauth/backend-fastapi">backend-fastapi
code</a></li>
</ul>
<p>I will explain the key points of implementing the login functionality
below.</p>
<h3 id="apilogin-endpoint">/api/login Endpoint</h3>
<p>The frontend app sends the JWT, and then the backend FastAPI app
verifies it using Google’s public certificates. If verification is
successful, the backend FastAPI app registers the user using the email
address in the JWT as the username in the user database. The information
of the newly created user and the session_id are registered in the
session database, and the session_id is set in a cookie in the
response.</p>
<p>Sample code for <code>auth/auth.py</code> is as follows:</p>
<pre><code>async def VerifyToken(jwt: str):
try:
idinfo = id_token.verify_oauth2_token(
jwt,
requests.Request(),
settings.google_oauth2_client_id)
except ValueError:
print("Error: Failed to validate JWT token with GOOGLE_OAUTH2_CLIENT_ID=" + settings.google_oauth2_client_id +".")
return None
print("idinfo: ", idinfo)
return idinfo
@router.post("/login")
async def login(request: Request, response: Response, ds: Session = Depends(get_db), cs: Session = Depends(get_cache)):
body = await request.body()
jwt = json.loads(body)["credential"]
if jwt == None:
return Response("Error: No JWT found")
print("JWT token: " + jwt)
idinfo = await VerifyToken(jwt)
if not idinfo:
print("Error: Failed to validate JWT token")
return Response("Error: Failed to validate JWT token")
user = await GetOrCreateUser(idinfo, ds)
if user:
user_dict = get_user_by_name(user.name, ds)
if not user_dict:
raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail="Error: User not exist in User table in DB.")
user = UserBase(**user_dict)
session_id = create_session(user, cs)
response.set_cookie(
key="session_id",
value=session_id,
httponly=True,
max_age=1800,
expires=1800,
)
else:
return Response("Error: Auth failed")
return {"Authenticated_as": user.name}</code></pre>
<h3 id="function-to-determine-active-users">Function to Determine Active
Users</h3>
<p>In the <code>get_current_user</code> function, FastAPI extracts the
session_id from the cookie of the received request and considers the
user logged in if it matches an entry in the session database. The
<code>get_current_active_user</code> checks whether the user is
disabled, and the <code>get_admin_user</code> checks whether the user
has admin privileges.</p>
<p>Sample code for <code>auth/auth.py</code> is as follows:</p>
<pre><code>async def get_current_user(ds: Session = Depends(get_db), cs: Session = Depends(get_cache), session_id: str = Depends(oauth2_scheme)):
if not session_id:
return None
session = get_session_by_session_id(session_id, cs)
if not session:
return None
username = session["name"]
user_dict = get_user_by_name(username, ds)
user=UserBase(**user_dict)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
)
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if not current_user:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="NotAuthenticated")
if current_user.disabled:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Inactive user")
return current_user
async def get_admin_user(current_user: User = Depends(get_current_active_user)):
print("CurrentUser: ", current_user)
if not current_user.admin:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Admin Privilege Required")
return current_user</code></pre>
<h3 id="protecting-various-endpoints">Protecting Various Endpoints</h3>
<p>The <code>/api/user/</code> endpoint is accessible only to logged-in
users due to <code>Depends(get_current_active_user)</code>.</p>
<p>Sample code for <code>auth/auth.py</code> is as follows:</p>
<pre><code>@router.get("/user/")
async def get_user(user: UserBase = Depends(get_current_active_user)):
return {"username": user.name, "email": user.email,}</code></pre>
<p>Routes defined in <code>customer/customer.py</code> are accessible
only to authenticated users, and those in <code>admin/user.py</code> are
accessible only to Admin users.</p>
<p>Sample code for <code>main.py</code> is as follows:</p>
<pre><code>import admin.debug, admin.user, auth.auth, auth.debug
import customer.customer
app = FastAPI()
app.include_router(
customer.customer.router,
prefix="/api",
tags=["CustomerForAuthenticatedUser"],
dependencies=[Depends(auth.auth.get_current_active_user)],
)
app.include_router(
admin.user.router,
prefix="/api",
tags=["AdminOnly"],
dependencies=[Depends(auth.auth.get_admin_user)],
)</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>I have implemented Google Sign-In functionality in a sample website
built using Svelte and FastAPI. After receiving the JWT from Google,
FastAPI issues a new session_id and maintains the session through
cookies. The session information is managed in FastAPI’s Session
database, allowing administrators to invalidate sessions anytime.
Additionally, adding Secure and HttpOnly attributes to the cookies can
prevent interception and JavaScript access, enabling a more secure
website development.</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-32309820385390689032023-12-25T23:40:00.015+09:002024-01-11T02:47:37.259+09:00Svelte+FastAPIでSign in with Googleを試してみた<h1 id="目次">目次</h1>
<ul>
<li><a href="#はじめに">はじめに</a></li>
<li><a href="#実装するもの">実装するもの</a></li>
<li><a
href="#svelteでのフロントエンドの実装">Svelteでのフロントエンドの実装</a></li>
<li><a
href="#fastapiでのバックエンド実装">FastAPIでのバックエンド実装</a></li>
<li><a href="#まとめ">まとめ</a></li>
</ul>
<h1 id="はじめに">はじめに</h1>
<p>SvelteとFastAPIを使用して構築したサンプルウェブサイトにGoogleのサインイン機能を実装しました。
Google Sign
Inに成功した後、バックエンドのAPIサーバーにログインするためには、様々な方法が考えられます。
Googleから受け取ったJWTを、Requestヘッダに<code>Authorization: "Bearer: JWT"</code>として送信し、正しいJWTであれば、認証されます。
また、バックエンドでJWTを発行し、<code>Authorization</code>ヘッダにセットして認証済みユーザーを識別する方法も一般的です。
しかし、JWTをそのままログインユーザーの識別に利用する場合、JWTの漏洩時に即時の無効化が難しい問題があります。
参考:<a
href="http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/">Stop
using JWT for sessions</a>。
そこで、GoogleからJWTを受け取った後、FastAPI側で新たにsession_idを発行し、Cookieを介してセッションを維持する方法で実装しました。</p>
<p>セッション情報はFastAPIのセッションデータベースで管理されており、管理者がいつでもセッションを無効にできます。
また、CookieにSecure属性とHttpOnly属性を付与することで、経路での盗聴防止やJavaScriptからのアクセス防止が可能になり、より安全なWebサイトの構築が可能です。</p>
<p>なお、SvelteもFastAPIも独学で学習中ですので、おかしな点があれば、アドバイスいただけると嬉しいです。</p>
<h1 id="実装するもの">実装するもの</h1>
<p>認証が実装されると、未ログイン時のアクセスはログインページにリダイレクトされ、そこでGoogleアカウントでログインできます。</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthLogin3-2.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthLogin3-2.png"
width="80%" alt="Login page" title="Login page"> </a></p>
<p>Customerページは、認証に成功した場合にのみ表示できます。</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthCustomer.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/AuthCustomer.png"
width="80%" alt="Customer page for authenticated users" title="Customer page for authenticated users">
</a></p>
<p>FastAPIではSwagger UIによるドキュメントページが自動生成されます。</p>
<p><a href="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/fastapi01.png"
target="_blank">
<img src="https://raw.githubusercontent.com/ktaka-ccmp/google-oauth2-example/v2.1.1/images/fastapi01.png"
width="80%" alt="FastAPI OpenAPI doc page" title="FastAPI OpenAPI doc page">
</a></p>
<h1
id="svelteでのフロントエンドの実装">Svelteでのフロントエンドの実装</h1>
<p>Svelteを使用してフロントエンドを実装します。
バックエンドからcustomerデータを取得し、テーブル表示するページにGoogle
OAuth2を利用した認証機能を実装します。</p>
<p>Google Sign
Inに成功し、取得したJWTをバックエンドのAPIサーバーに送信します。
バックエンド側では、JWTをベリファイしユーザーアカウントを作成し、session_idをCookieにセットしてレスポンスを返信します。
これ以降、バックエンドへのリクエスト時には、常にCookieにsession_idをセットして送信します。</p>
<p>実装したコードは以下のリポジトリにあります。</p>
<ul>
<li><a
href="https://github.com/ktaka-ccmp/google-oauth2-example/tree/v2.1.1/google-oauth/frontend-svelte">frontend-svelteのコード</a></li>
</ul>
<p>ログイン機能の実装ポイントを以下に説明します。</p>
<h2 id="ルーティング">ルーティング</h2>
<p>svelete-routingを利用し、以下のようにルーティングを設定します。</p>
<ul>
<li><strong>/customer</strong>:
Customerコンポーネントを表示します。</li>
<li><strong>/login</strong>: LoginPageコンポーネントを表示します。</li>
</ul>
<p><code>App.svelte</code>のサンプルコードは次の通りです。</p>
<pre class="svelte"><code><script>
import { Router, Link, Route } from "svelte-routing";
import Top from "./components/Top.svelte";
import Customer from "./components/Customer.svelte";
import NoMatch from "./components/NoMatch.svelte";
import LoginPage from "./components/LoginPage.svelte";
export let url = "";
</script>
<div class="container-sm">
<Router {url}>
<nav>
<table class="table-borderless table-responsive">
<tbody>
<tr><td><Link to="/">Top</Link></td></tr>
<tr><td><Link to="/customer">Customer</Link></td></tr>
</tbody>
</table>
</nav>
<div>
<Route path="/"><Top /></Route>
<Route path="/customer"><Customer /></Route>
<Route path="/login"><LoginPage /></Route>
<Route path="*"><NoMatch /></Route>
</div>
</Router>
</div></code></pre>
<h2 id="ログインページ">ログインページ</h2>
<p>GoogleのSign Inボタンを表示し、OneTapインターフェースも利用します。
GoogleでSign
In後、コールバックファンクション<code>backendAuth</code>を呼び出します。
<code>backendAuth</code>では、Google Sign
Inで得られたレスポンスを<code>http://localhost/api/login</code>に送信します。
レスポンスにはJWTトークンが含まれます。
バックエンドでのログインが成功した場合、直前にいたページにリダイレクトします。
失敗した場合、エラー処理が行われ、再度ログインページにリダイレクトされます。</p>
<p><code>LoginPage.svelte</code>のサンプルコードは次の通りです。</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios";
import { useLocation, navigate } from "svelte-routing";
import { jwtDecode } from "jwt-decode";
let location = useLocation();
let origin = $location.state?.from;
const backendAuth = (response) => {
const data = JSON.stringify(response, null, 2);
console.log("JWT fed to backendAuth:\n", data);
apiAxios
.post(`/api/login/`, data)
.then((res) => {
console.log("Navigate back to: ", origin);
navigate(origin, { replace: true });
})
.catch((error) => {
console.log("backendAuth failed. Redirecting to /login... ");
});
};
const onLogin = backendAuth;
onMount(() => {
google.accounts.id.initialize({
/* global google */
client_id: import.meta.env.VITE_APP_GOOGLE_OAUTH2_CLIENT_ID,
callback: (r) => onLogin(r),
ux_mode: "popup",
// ux_mode: "redirect",
});
google.accounts.id.renderButton(document.getElementById("signInDiv"), {
theme: "filled_blue",
size: "large",
shape: "circle",
});
google.accounts.id.prompt();
});
</script>
<main>
<h2>Login page</h2>
<div id="signInDiv"></div>
</main></code></pre>
<h2
id="axiosインスタンスのセットアップ">axiosインスタンスのセットアップ</h2>
<p><code>withCredentials: true</code>をセットすることでaxiosはCookieを送信するようになります。
axiosのinterceptorsでエラー処理を行い、バックエンドから<code>401 Unauthorized</code>、<code>403 Forbidden</code>が返ってきた場合、<code>/login</code>へリダイレクトします。</p>
<p><code>apiAxios.js</code>のサンプルコードは次の通りです。</p>
<div class="sourceCode" id="cb3"><pre
class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> axios <span class="im">from</span> <span class="st">"axios"</span><span class="op">;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> { navigate } <span class="im">from</span> <span class="st">"svelte-routing"</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="im">export</span> <span class="kw">const</span> apiAxios <span class="op">=</span> axios<span class="op">.</span><span class="fu">create</span>({</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">baseURL</span><span class="op">:</span> <span class="vs">`</span><span class="sc">${</span><span class="im">import</span><span class="op">.</span><span class="at">meta</span><span class="op">.</span><span class="at">env</span><span class="op">.</span><span class="at">VITE_APP_API_SERVER</span><span class="sc">}</span><span class="vs">`</span><span class="op">,</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> <span class="dt">withCredentials</span><span class="op">:</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>apiAxios<span class="op">.</span><span class="at">interceptors</span><span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="fu">use</span>(</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> (response) <span class="kw">=></span> {</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> response<span class="op">;</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> (error) <span class="kw">=></span> {</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (error<span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="at">status</span> <span class="op">===</span> <span class="dv">401</span> <span class="op">||</span> error<span class="op">.</span><span class="at">response</span><span class="op">.</span><span class="at">status</span> <span class="op">===</span> <span class="dv">403</span>) {</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="st">"apiAxios failed. Redirecting to /login... from"</span><span class="op">,</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> location<span class="op">.</span><span class="at">pathname</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="fu">navigate</span>(<span class="st">"/login"</span><span class="op">,</span> { <span class="dt">state</span><span class="op">:</span> { <span class="dt">from</span><span class="op">:</span> location<span class="op">.</span><span class="at">pathname</span> }<span class="op">,</span> <span class="dt">replace</span><span class="op">:</span> <span class="kw">true</span> })<span class="op">;</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">Promise</span><span class="op">.</span><span class="fu">reject</span>(error)<span class="op">;</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></code></pre></div>
<h2 id="logoutbuttonコンポーネント">LogoutButtonコンポーネント</h2>
<p>Logoutボタンを表示するコンポーネントです。
onMount時に、バックエンドサーバにアクセスし、ログインしているユーザーの情報を取得します。
Cookieにsession_idが無い場合、すなわち未ログインの場合にはユーザー情報取得に失敗し、apiAxios.interceptorのエラー処理により、<code>/login</code>ページにリダイレクトされます。</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios.js";
let user;
onMount(() => {
console.log("Logout Component Mounted");
getUser();
});
const handleLogout = () => {
user = null;
apiAxios
.get(`/api/logout/`)
.then((res) => {
console.log("backendLogout", res);
getUser();
})
.catch((error) => console.log("Logout failed: ", error));
};
const getUser = () => {
apiAxios
.get(`/api/user/`)
.then((res) => {
user = res.data;
console.log("getUser: user:", user);
})
.catch((error) => console.log("getUser failed: ", error.response));
};
const onLogout = handleLogout;
</script>
<div>
Authenticated as {user?.username} &nbsp;
<button type="button" on:click={onLogout}>Sign Out</button>
</div></code></pre>
<h2 id="customerコンポーネント">Customerコンポーネント</h2>
<p>バックエンドサーバからデータを取得し、テーブル表示するコンポーネントです。<code>LogoutButton</code>
コンポーネントがページ内に配置されているので、未ログインの場合には、<code>/login</code>
ページにリダイレクトされます。</p>
<pre class="svelte"><code><script>
import { onMount } from "svelte";
import { apiAxios } from "../lib/apiAxios";
import LogoutButton from "./LogoutButton.svelte";
let customers = [];
let Loading = true;
onMount(async () => {
await new Promise((r) => setTimeout(r, 1000));
apiAxios
.get(`/api/customer/`)
.then((res) => (customers = res.data.results))
.catch((error) => console.log(error))
.finally(() => Loading = false);
});
</script>
<LogoutButton />
<h2>This is Customer.</h2>
{#if Loading}
<p>Loading ...</p>
{:else}
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead class="table-light">
<tr>
<th>id</th>
<th>name</th>
<th>email</th>
</tr>
</thead>
<tbody>
{#each customers as cs}
<tr>
<td>{cs.id}</td>
<td>{cs.name}</td>
<td>{cs.email}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}</code></pre>
<h1 id="fastapiでのバックエンド実装">FastAPIでのバックエンド実装</h1>
<p>FastAPIを使用して、バックエンドのAPIサーバを実装します。
フロントエンドから受け取ったJWTを検証し、ユーザーアカウントを作成して、session_idを発行しセッションデータベースに登録します。
作成したsession_idをCookieにセットしてレスポンスを返信します。
受け取ったJWTに対応するユーザーがデータベースに存在しない場合、新たにユーザーを作成します。</p>
<p>認証で保護されたエンドポイントへのリクエストを受け取った場合、Cookieにセットされたsession_idとセッションデータベースを照合し、有効なセッション情報が存在している場合のみ、要求されたデータを返信します。</p>
<p>実装したコードは以下のリポジトリにあります。</p>
<ul>
<li><a
href="https://github.com/ktaka-ccmp/google-oauth2-example/tree/v2.1.1/google-oauth/backend-fastapi">backend-fastapiのコード</a></li>
</ul>
<p>ログイン機能の実装ポイントについて以下に説明します。</p>
<h2 id="apiloginエンドポイント">/api/loginエンドポイント</h2>
<p>フロントエンドからJWTを受け取り、Googleの公開証明書を使用してJWTを検証します。
検証に成功すると、JWT内のemailアドレスを使用してユーザーデータベースにユーザーを登録します。
新しく作成したユーザーの情報とsession_idをセッションデータベースに登録し、Cookieにsession_idをセットしてレスポンスを返します。</p>
<p>auth/auth.py</p>
<pre><code>async def VerifyToken(jwt: str):
try:
idinfo = id_token.verify_oauth2_token(
jwt,
requests.Request(),
settings.google_oauth2_client_id)
except ValueError:
print("Error: Failed to validate JWT token with GOOGLE_OAUTH2_CLIENT_ID=" + settings.google_oauth2_client_id +".")
return None
print("idinfo: ", idinfo)
return idinfo
@router.post("/login")
async def login(request: Request, response: Response, ds: Session = Depends(get_db), cs: Session = Depends(get_cache)):
body = await request.body()
jwt = json.loads(body)["credential"]
if jwt == None:
return Response("Error: No JWT found")
print("JWT token: " + jwt)
idinfo = await VerifyToken(jwt)
if not idinfo:
print("Error: Failed to validate JWT token")
return Response("Error: Failed to validate JWT token")
user = await GetOrCreateUser(idinfo, ds)
if user:
user_dict = get_user_by_name(user.name, ds)
if not user_dict:
raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail="Error: User not exist in User table in DB.")
user = UserBase(**user_dict)
session_id = create_session(user, cs)
response.set_cookie(
key="session_id",
value=session_id,
httponly=True,
max_age=1800,
expires=1800,
)
else:
return Response("Error: Auth failed")
return {"Authenticated_as": user.name}</code></pre>
<h2
id="アクティブユーザーを判別する関数">アクティブユーザーを判別する関数</h2>
<p>FastAPIが受け取ったリクエストのCookieからsession_idを取り出し、セッションデータベース内のエントリと一致すればログイン済みとみなします。
<code>get_current_active_user</code>では、disabledのフラグが立っていないか判別し、<code>get_admin_user</code>では、adminのフラグが立っているかどうか判別します。</p>
<p>auth/auth.py</p>
<pre><code>async def get_current_user(ds: Session = Depends(get_db), cs: Session = Depends(get_cache), session_id: str = Depends(oauth2_scheme)):
if not session_id:
return None
session = get_session_by_session_id(session_id, cs)
if not session:
return None
username = session["name"]
user_dict = get_user_by_name(username, ds)
user=UserBase(**user_dict)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
)
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if not current_user:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="NotAuthenticated")
if current_user.disabled:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Inactive user")
return current_user
async def get_admin_user(current_user: User = Depends(get_current_active_user)):
print("CurrentUser: ", current_user)
if not current_user.admin:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Admin Privilege Required")
return current_user</code></pre>
<h2 id="各種エンドポイントの保護">各種エンドポイントの保護</h2>
<p><code>Depends(get_current_active_user)</code>により、<code>/api/user/</code>エンドポイントはログインユーザーのみがアクセスできます。</p>
<p>auth/auth.py</p>
<pre><code>@router.get("/user/")
async def get_user(user: UserBase = Depends(get_current_active_user)):
return {"username": user.name, "email": user.email,}</code></pre>
<p><code>customer/customer.py</code>で定義されたルートは認証済みユーザーのみ、<code>admin/user.py</code>で定義されたルートはAdminユーザーのみがアクセスできます。</p>
<p>main.py</p>
<pre><code>import admin.debug, admin.user, auth.auth, auth.debug
import customer.customer
app = FastAPI()
app.include_router(
customer.customer.router,
prefix="/api",
tags=["CustomerForAuthenticatedUser"],
dependencies=[Depends(auth.auth.get_current_active_user)],
)
app.include_router(
admin.user.router,
prefix="/api",
tags=["AdminOnly"],
dependencies=[Depends(auth.auth.get_admin_user)],
)</code></pre>
<h1 id="まとめ">まとめ</h1>
<p>SvelteとFastAPIを用いて構築したサンプルウェブサイトにGoogleのサインイン機能を実装しました。
GoogleからJWTを受け取った後、FastAPI側で新たにsession_idを発行し、Cookieを介してセッションを維持する方法で実装しました。
セッション情報はFastAPIのセッションデータベースで管理されており、いつでも管理者がセッションを無効にできます。
また、CookieにSecure属性とHttpOnly属性を付与することで、経路での盗聴防止やJavaScriptからのアクセス防止が可能になり、より安全なWebサイトの構築が可能です。</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-59702501321409702692023-04-09T00:47:00.023+09:002023-12-26T13:35:44.310+09:00UEFI/BIOSのどちらでもGRUB起動可能なUSBメモリを作る<h1 id="目次">目次</h1>
<ul>
<li><a href="#要旨">要旨</a></li>
<li><a href="#はじめに">はじめに</a></li>
<li><a href="#やり方">やり方</a></li>
<li><a href="#実際にブートできるのか">実際にブートできるのか</a></li>
<li><a href="#まとめ">まとめ</a></li>
</ul>
<h2 id="要旨">要旨</h2>
<p>UEFIモードとBIOSモードのどちらでもGRUBを起動可能なUSBメモリの作成を試みた。
どちらの場合でもGRUBの起動が可能であるはずのUSBメモリを作成した。
SupermicroワークステーションとIntel
NUCで実機検証したところ、Supermicroのワークステーションでは、どちらのモードでもブート可能であったが、Intel
NUCはそうではなかった。
ハードウェアによってはBIOSブート時にパーティションにbootable(active)フラグが必要であるものがあり、その場合は、UEFIブートと両立できないことがわかった。</p>
<h2 id="はじめに">はじめに</h2>
<p>USBメモリからLinuxが起動できると便利である。サーバのメンテナンスや新規OSインストールなどの様々な場面で、ディスクレスなLinuxをUSBメモリから起動し役立てることができる。私の場合、自宅や会社のルータとして、USBから起動したDebian
Linuxを利用している。ファイルシステムをメモリ(tmpfs)上に展開しディスクレスで運用しているので、なにか設定をしくじった場合にも再起動すればもとの状態に戻すことができるし、ハードウェアが故障してしまった場合などは別のマシンにUSBメモリを差し替えれば簡単に復旧できるので大変重宝している。</p>
<p>Linuxを起動するにはGRUBというブートローダを利用することが多い。GRUBはLinuxカーネルのロードが可能なだけでなく、Windowsのチェインロードも可能である。
かつてのPCは、BIOSというファームウェアがドライブの先頭にあるGRUBをロードしていたが、最近のPCではUEFIがESPにあるGRUBをロードするように仕様に変わった。
BIOSとUEFIではブート仕様が異なるので、GRUBのインストールの方式も異なっていて、一方の方式でLinux及びGRUBインストールしたドライブをもう一方の方式のPCで利用することができない。通常は、HDDやSSD等の起動ドライブをPC間で移動することはあまりないのでそれぞれのPCにそれぞれ適した方式でインストールすれば問題ない。</p>
<p>しかしメンテナンス用のUSBメモリは、できれば、古いBIOS方式でも新しいUEFI方式でも、どちらでも使えるようにしておきたい。
そこで、今回、UEFIモードでもBIOSモードでも、GRUBを起動することができるUSBメモリの作成を試みた。</p>
<h2 id="やり方">やり方</h2>
<p>LinuxパソコンにUSBメモリを挿したとき、/dev/sdaとして認識されているとし、そこにパーティションテーブルを作成しGRUBのインストールを行う。
以下の手順は、ストレージのパーティションを操作するので、/dev/sdaが確かに操作しようとしているUSBメモリなのかをよく確認してから行う必要がある。</p>
<h3 id="パーティション初期化と作成">パーティション初期化と作成</h3>
<p>Linuxのルートパーティション、ESPパーティション、BIOSブートパーティションを作成する。ESPパーティションは先頭から512Mbyte、Linuxルートパーティションは残り全部を割り当てる。最近のパーティションツールは、先頭の先頭に2047セクタ分の空きを残すようなので、そこにBIOSブートパーティションを作成する。</p>
<pre><code>TOP_DIR=./
dev=/dev/sda
# Clear partition table
sgdisk -Z $dev
# Create ESP partition
sgdisk -n 2::+512M $dev
sgdisk -t 2:ef00 $dev
# Create Linux partition
sgdisk -n 1:: $dev
# Create BIOS boot partition
sgdisk -a 1 -n 3:34:2047 $dev
sgdisk -t 3:ef02 $dev
# Change the GPT name of each partition
sgdisk -c 1:Linux -c 2:ESP -c 3:BIOS $dev
# Show current status
sgdisk -p $dev</code></pre>
<p>LinuxルートパーティションとESPパーティションをフォーマットし、ファイルシステムにマウント。</p>
<pre><code># Format ESP and Linux partition
mkfs.fat -F32 -n ESP ${dev}2
mkfs.ext4 -F -L usbdebian ${dev}1
# Create mount point and mount file systems
mkdir -p ${TOP_DIR}/mnt
mount -L usbdebian ${TOP_DIR}/mnt
mkdir -p ${TOP_DIR}/mnt/boot/efi
mount -L ESP ${TOP_DIR}/mnt/boot/efi</code></pre>
<p>BIOS用GRUBのインストール。MBR(ディスクの先頭512Byte)とBIOSブートパーティションにgrubをインストール。</p>
<pre><code># BIOS setup
grub-install --target=i386-pc --boot-directory=./mnt/boot/ $dev</code></pre>
<p>動作確認用のGRUBメニューの設定ファイルを作成。今回はLinuxカーネルは入れず、このメニューが表示できるところまでがゴールなので、仮のもので良い。</p>
<pre><code># Create minimum grub.cfg for testing
cat << \EOF > ${TOP_DIR}/mnt/boot/grub/grub.cfg
set default=1
set timeout=5
menuentry "Debian Linux" {
linux /boot/vmlinuz
}
EOF</code></pre>
<p>UEFIモードでブートするには、UEFI設定にブートするバイナリとその優先順位を登録することが一般的であるが、USBメモリの場合リームーバブルメディアであるので適さない。
UEFIブートではUEFI設定で他に指定がない場合、EFI/boot/bootx64.efiにフォールバックするので、grubx64.efiをその名前で置いておけば良い。grubx64.efiは同じディレクトリのgrub.cfgを参照するので、そこに/boot/grub/grub.cfgを更に参照するように書いておく。</p>
<p>UEFIモードの場合にはセキュアブートも可能である。その場合には、shimx64.efi
-> grubx64.efi ->
grub.cfgの順で読み込ませたいので、shimx64.efiをEFI/boot/bootx64.efiとして置き、同じディレクトリに、grubx64.efiとgrub.cfgを置けば良い。この時、shimx64.efiとgrubx64.efiは署名済みのものを用いる必要がある。</p>
<pre><code># UEFI boot setup
mkdir -p ${TOP_DIR}/mnt/boot/efi/EFI/boot
cp /usr/lib/grub/x86_64-efi/monolithic/grubx64.efi ${TOP_DIR}/mnt/boot/efi/EFI/boot/bootx64.efi
cat << \EOF > ${TOP_DIR}/mnt/boot/efi/EFI/boot/grub.cfg
search.fs_label usbdebian root
set prefix=($root)'/boot/grub'
configfile $prefix/grub.cfg
EOF
# Unmount filesystem
umount ${TOP_DIR}/mnt/boot/efi/ && umount ${TOP_DIR}/mnt/</code></pre>
<p>作成したUSBメモリのパーティション構成</p>
<pre><code>root@dyna:~# sgdisk -p /dev/sda
Disk /dev/sda: 60088320 sectors, 28.7 GiB
Model: Ultra Fit
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 21780B2C-8682-42A0-9564-D7E7D726A7EC
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 60088286
Partitions will be aligned on 2-sector boundaries
Total free space is 0 sectors (0 bytes)
Number Start (sector) End (sector) Size Code Name
1 1050624 60088286 28.2 GiB 8300 Linux
2 2048 1050623 512.0 MiB EF00 ESP
3 34 2047 1007.0 KiB EF02 BIOS</code></pre>
<p>作成したUSBメモリのファイルシステム確認(unmout前に確認)</p>
<pre><code>root@dyna:~# tree -L 3 ${TOP_DIR}/mnt/
mnt/
├── boot
│ ├── efi
│ │ └── EFI
│ └── grub
│ ├── fonts
│ ├── grub.cfg
│ ├── grubenv
│ ├── i386-pc
│ └── locale
└── lost+found
8 directories, 2 files
root@dyna:~# tree ${TOP_DIR}/mnt/boot/efi/
mnt/boot/efi/
└── EFI
└── boot
├── bootx64.efi
└── grub.cfg
2 directories, 2 files</code></pre>
<p>以上でGRUBまで立ち上がるUSBは完成である。</p>
<h2 id="実際にブートできるのか">実際にブートできるのか</h2>
<p>作成したUSBメモリで実際にブート可能であるかを、SupermicroのワークステーションとIntel
NUCで試してみた。
Supermicroのワークステーションでは、UEFIブートであってもBIOSブートであっても、GRUBメーニュー画面が表示できた。
Intel
NUCの場合は、UEFIモードでは無事にGRUBのメニューが表示されたが、BIOSモードでのブートができなかった。</p>
<p>Intel
NUCの場合には、BIOSモードでブートする場合、次の手順でPMBRのパーティションテーブルでbootable(active)フラグを立ててやる必要があった。</p>
<pre><code>root@dyna:~# sfdisk -A /dev/sda 1
Activation is unsupported for GPT -- entering nested PMBR.
The bootable flag on partition 1 is enabled now.
The partition table has been altered.
Syncing disks.
root@dyna:~# sgdisk -O /dev/sda
Disk size is 60088320 sectors (28.7 GiB)
MBR disk identifier: 0x00000000
MBR partitions:
Number Boot Start Sector End Sector Status Code
1 * 1 60088319 primary 0xEE </code></pre>
<p>PMBRのパーティションテーブルでbootable(active)フラグを立てた場合、今度はUEFIブートができなくなってしまった。どうやらUEFIブートのUEFIブートの規格でPMBR上でそのフラグを立てることを明確に禁止しているらしい。
再度、UEFIブートするには、以下のように、bootable(active)フラグを戻す必要がある。</p>
<pre><code>root@dyna:~# sfdisk -A /dev/sda -
Activation is unsupported for GPT -- entering nested PMBR.
The bootable flag on partition 1 is disabled now.
The partition table has been altered.
Syncing disks. </code></pre>
<p>古いハードウェアには、bootable(active)フラグが立っているパーティションが無いとBIOSブートできないものがあり、どうやらIntel
NUCはそのようなハードウェアのひとつであるようだ。</p>
<p>参考</p>
<ul>
<li><a
href="https://wiki.archlinux.org/title/Partitioning#Tricking_old_BIOS_into_booting_from_GPT">Partitioning
- wiki.archlinux.org</a> (このやり方はうまく行かなかった)</li>
<li><a href="https://unix.stackexchange.com/a/325899">BIOS / GPT: do we
need a ‘boot’ flag?</a></li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>今回、UEFIモードでもBIOSモードでも、GRUBを起動することができるUSBメモリの作成を試みた。
作成したUSBメモリでGRUB画面の表示までたどり着けるか試してみた。
Supermicroのワークステーションでは、BIOSブート、UEFIブートのどちらでもGRUB画面にたどり着くことができたが、Intel
NUCではPMBR上でbootable(active)フラグの切り替えが必要であった、</p>
<p>UEFIブートの規格でPMBR上でそのフラグを立てることを明確に禁止しているらしい。
しかし古いハードウェアにはBIOSブート時に、bootable(active)フラグが立っているパーティションが必要であるものがあることが確認された。
その場合には、ブートモードに応じて作成したUSBメモリのbootable(active)フラグを切り替えることがどうしても必要になってしまうことがわかった。</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-44330451204216925362023-03-22T18:34:00.057+09:002024-01-26T18:24:11.096+09:00KVM再入門:Qemuコマンドラインで仮想マシンを起動し、Windows11ゲストをセットアップする。<h1 id="目次">目次</h1>
<ul>
<li><a href="#目次">目次</a>
<ul>
<li><a href="#はじめに">はじめに</a></li>
<li><a href="#window11のインストール">Window11のインストール</a>
<ul>
<li><a href="#kvm仮想マシンの起動方法">KVM仮想マシンの起動方法</a></li>
<li><a
href="#仮想マシンが立ち上がるとuefシェルが起動してしまう">仮想マシンが立ち上がると、UEFシェルが起動してしまう。</a></li>
<li><a
href="#インストール先のドライブが見えない">インストール先のドライブが見えない</a></li>
<li><a
href="#インストール時にmicrosoftアカウントへのサインインを求められる">インストール時にMicrosoftアカウントへのサインインを求められる</a></li>
<li><a
href="#残りのvirtioドライバーをインストール">残りのvirtioドライバーをインストール</a></li>
</ul></li>
<li><a href="#普段使いのために">普段使いのために</a>
<ul>
<li><a
href="#linuxホストのネットワークセットアップ">Linuxホストのネットワークセットアップ</a></li>
<li><a
href="#windowsゲスト起動コマンド">Windowsゲスト起動コマンド</a></li>
<li><a
href="#windowsゲストのネットワーク設定">Windowsゲストのネットワーク設定</a></li>
</ul></li>
<li><a href="#その他のtips">その他のTips</a></li>
<li><a href="#参考文献">参考文献</a>
<ul>
<li><a href="#qemuでのtpmの使い方">QemuでのTPMの使い方</a></li>
<li><a href="#qemuでのuefi-secure-bootのやり方">QEMUでのUEFI Secure
Bootのやり方</a></li>
<li><a href="#どのuefi-firmwareを使うべきか">どのUEFI
firmwareを使うべきか</a></li>
</ul></li>
<li><a href="#まとめ">まとめ</a></li>
</ul></li>
</ul>
<h2 id="はじめに">はじめに</h2>
<p><a
href="/2023/03/kvmqemuwindows.html">前回</a>に引き続き、KVM仮想マシン上にWindows11をインストールするやり方をまとめておきます。</p>
<p>Windows10の時とは以下のような違いがあります。</p>
<ul>
<li>Windows11の場合、TPMとUEFI Secure Bootが必須である。</li>
<li>KVMでTPMを使うには、ホストの/dev/tpm0をパススルーで使う方法と、ソフトウェアのTPMデバイスエミュレーター(swtpm)を使う方法がある。</li>
<li>KVMでUEFI Secure Bootするには、ovmfパッケージにより提供されるUEFI
firmwareを利用する。</li>
</ul>
<h2 id="window11のインストール">Window11のインストール</h2>
<h3 id="kvm仮想マシンの起動方法">KVM仮想マシンの起動方法</h3>
<p>Windows11のインストールメディア、virtioドライバインストール用のisoイメージをダウンロードしておきます。</p>
<ul>
<li><a
href="https://www.microsoft.com/en-us/software-download/windows11">Download
Windows 11</a></li>
<li><a
href="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/">Windows用virtioドライバのありか</a></li>
</ul>
<p>必要パッケジージのインストール</p>
<pre><code>sudo apt-get install qemu-system-x86 virt-viewer ovmf</code></pre>
<p>qcow2ディスクイメージを作成します。</p>
<pre><code>qemu-img create -f qcow2 win11pro.qcow2 40G</code></pre>
<p>書き込み用のFirmwareのローカルコピーを作成します。</p>
<pre><code>cp /usr/share/OVMF/OVMF_VARS_4M.ms.fd ./</code></pre>
<h4
id="ホストのdevtpm0をパススルーで使う場合">ホストの/dev/tpm0をパススルーで使う場合</h4>
<p>以下のコマンドで仮想マシンを起動できます。</p>
<pre><code>sudo qemu-system-x86_64 \
-machine q35,accel=kvm \
-m 8192 -cpu host \
-smp 6,sockets=1,dies=1,cores=6,threads=1 \
-display spice-app \
-drive file=./win11pro.qcow2,if=virtio,format=qcow2,discard=unmap \
-drive file=~/Downloads/Win11_22H2_Japanese_x64v1.iso,index=0,media=cdrom \
-drive file=~/Downloads/virtio-win-0.1.229.iso,index=1,media=cdrom \
-drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE_4M.ms.fd,readonly=on \
-drive if=pflash,format=raw,file=./OVMF_VARS_4M.ms.fd \
-tpmdev passthrough,id=tpm0,path=/dev/tpm0,cancel-path=/dev/null \
-device tpm-tis,tpmdev=tpm0 \</code></pre>
<h4
id="ソフトウェアのtpmデバイスエミュレータを使う場合">ソフトウェアのTPMデバイスエミュレータを使う場合</h4>
<p>TPMデバイスエミュレータswtpmを起動しておきます。</p>
<pre><code>mkdir mytpm
swtpm socket --tpmstate dir=./mytpm \
--tpm2 \
--ctrl type=unixio,path=./mytpm/swtpm-sock \
--log level=20</code></pre>
<p>以下のコマンドで仮想マシンを起動できます。</p>
<pre><code>sudo qemu-system-x86_64 \
-machine q35,accel=kvm \
-m 8192 -cpu host \
-smp 6,sockets=1,dies=1,cores=6,threads=1 \
-display spice-app \
-drive file=./win11pro.qcow2,if=virtio,format=qcow2,discard=unmap \
-drive file=~/Downloads/Win11_22H2_Japanese_x64v1.iso,index=0,media=cdrom \
-drive file=~/Downloads/virtio-win-0.1.229.iso,index=1,media=cdrom \
-drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE_4M.ms.fd,readonly=on \
-drive if=pflash,format=raw,file=./OVMF_VARS_4M.ms.fd \
-chardev socket,id=chrtpm,path=./mytpm/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis,tpmdev=tpm0 \</code></pre>
<h3
id="仮想マシンが立ち上がるとuefシェルが起動してしまう">仮想マシンが立ち上がると、UEFシェルが起動してしまう。</h3>
<p>どうやらUEFIブートの場合、UEFIの設定画面でブートデバイスの優先順位を変更してやる必要があるようです。
UEFIシェルをexitで抜けるとUEFIの設定メニューに入るので、そこでUEFIシェルの優先順位をCDROM/DVDROMなどインストールメディアよりも低くします。
そして、RestまたはContinueで、Windowsインストール用のCDROMからブートします。</p>
<!--[<img src="" width="50%">]() -->
<!-- <a href="" target="_blank"><img src="" width="30%"></a> -->
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihrFFZMkVRwJR1x-xab4eUrN5p6I7c62JEHufK8YTtEkCxEBmDekjeremksk36dFa2iVpHmYEfADbCMCpmqkXeXjxu5ALDkteHK3qXBNf24NC7H13pf3XH9NRoajeW-_9GC2zuehDbvF9TOoPTuE7N8tmcUmQ0DT-W57VMcHD-x7oM9gbG-2ZvMos2/s812/UEFI_Interactive_Shell01.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihrFFZMkVRwJR1x-xab4eUrN5p6I7c62JEHufK8YTtEkCxEBmDekjeremksk36dFa2iVpHmYEfADbCMCpmqkXeXjxu5ALDkteHK3qXBNf24NC7H13pf3XH9NRoajeW-_9GC2zuehDbvF9TOoPTuE7N8tmcUmQ0DT-W57VMcHD-x7oM9gbG-2ZvMos2/s812/UEFI_Interactive_Shell01.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqHreJWPEoR0Fpe_jIgQraetmCskhYNlXq1lbhvB6TOO2J3cxcsAmqnRcdZyhHXrP2IfwjHl3q4Cv4uKjuihbAqCBph6QmXUzK1Lvn8pAFDxQ_kjhHv-kjxUju-VHeV53POKiUNGVlf49nKKsJHExJpJ6zTLZdR0H2F651Yz1mA--UPJ1m6nyljSas/s652/UEFI_Menu03_2.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqHreJWPEoR0Fpe_jIgQraetmCskhYNlXq1lbhvB6TOO2J3cxcsAmqnRcdZyhHXrP2IfwjHl3q4Cv4uKjuihbAqCBph6QmXUzK1Lvn8pAFDxQ_kjhHv-kjxUju-VHeV53POKiUNGVlf49nKKsJHExJpJ6zTLZdR0H2F651Yz1mA--UPJ1m6nyljSas/s652/UEFI_Menu03_2.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLlqsFcDN-bPtB0kXDk5afq8Z00njv8JZAuTfKN-wsO8rKjY9KoGT7PNaqtUZ2WrKR3YxKiD2sdy55TLNC3Xd4TicCVA97ihFcd4vbcOLHst_cdqrWLhR9RUJLPKalK3w2460Tg55Yr49Ky3u86vbhh-FtuLOeJxaViFVtQreL6vfb0XEfcbsoFJuo/s652/UEFI_Menu05_2.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLlqsFcDN-bPtB0kXDk5afq8Z00njv8JZAuTfKN-wsO8rKjY9KoGT7PNaqtUZ2WrKR3YxKiD2sdy55TLNC3Xd4TicCVA97ihFcd4vbcOLHst_cdqrWLhR9RUJLPKalK3w2460Tg55Yr49Ky3u86vbhh-FtuLOeJxaViFVtQreL6vfb0XEfcbsoFJuo/s652/UEFI_Menu05_2.png" width="30%"></a></p>
<h3
id="インストール先のドライブが見えない">インストール先のドライブが見えない</h3>
<p>E:ドライブ(virtio-win-xx)にあるドライバを読み込むと、qcow2のディスクイメージが見えるようになるので、そこにWindows11をインストールします。</p>
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPRUFpYwiM6pVv34_N62FADeDOf81gAY92gIJoY5MSI60k7KzcMDMgenuVKqNp_sffAyBDif-5s4GMCeW2yshRsw954np5BvRxYkNjExSJi522HXSUxiUcUsQuJu-EDC0Euqvmod6gwkn1SrWCz8QFbsEri07lV9gAmbrJRBJPQfgjD_dYM1coC5xS/s812/Win11Installer03.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPRUFpYwiM6pVv34_N62FADeDOf81gAY92gIJoY5MSI60k7KzcMDMgenuVKqNp_sffAyBDif-5s4GMCeW2yshRsw954np5BvRxYkNjExSJi522HXSUxiUcUsQuJu-EDC0Euqvmod6gwkn1SrWCz8QFbsEri07lV9gAmbrJRBJPQfgjD_dYM1coC5xS/s812/Win11Installer03.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiieg21QaYwI425apZUogwiSTcIAkZzddzzRVGDty6NHfGoSOH3U_zi2lRbuwMgoGdkOzChtbUq8E7hVlG44Gro40C5Ia_FMj1qtaEPLSD9IgOGBvlYQiRjC1xXQjEyMa8cgCiTl1SK546K9zNRFzNXnr0XPc1MS4n0FdY18GN218YlE1M4xfI_HPUz/s812/Win11Installer04.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiieg21QaYwI425apZUogwiSTcIAkZzddzzRVGDty6NHfGoSOH3U_zi2lRbuwMgoGdkOzChtbUq8E7hVlG44Gro40C5Ia_FMj1qtaEPLSD9IgOGBvlYQiRjC1xXQjEyMa8cgCiTl1SK546K9zNRFzNXnr0XPc1MS4n0FdY18GN218YlE1M4xfI_HPUz/s812/Win11Installer04.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj56FVNkvw7VGKBYY4J9UCf35bImgTQlvElka3lBn-9juHZjYUderh1dnnuuX0BExKce48fEdomkMBaANzi1OKPZVsA6szkgdS2uvjgxx2XN4qFP3oDCGuh2PWq52I8HlFcG-2_nHNh0VBe18r6ygCJJjldeFiTcrdn8nYwjHGbXFpnbjexDPVvRvj/s812/Win11Installer06.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj56FVNkvw7VGKBYY4J9UCf35bImgTQlvElka3lBn-9juHZjYUderh1dnnuuX0BExKce48fEdomkMBaANzi1OKPZVsA6szkgdS2uvjgxx2XN4qFP3oDCGuh2PWq52I8HlFcG-2_nHNh0VBe18r6ygCJJjldeFiTcrdn8nYwjHGbXFpnbjexDPVvRvj/s812/Win11Installer06.png" width="30%"></a></p>
<h3
id="インストール時にmicrosoftアカウントへのサインインを求められる">インストール時にMicrosoftアカウントへのサインインを求められる</h3>
<p>no@thankyou.comで一度サインインに失敗すると、ローカルアカウントが作成できます。</p>
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDKU4bzc8PYfvrRNVgoQ038q5T9ZVFg2ebpZKjOdwh7njqrTQcp1jEsS3KmGgLqloBInsW-WJu3coySNApuL4JocTbTH6UcridWunnAqF_wPKt0t7OQfpoDc4SY8EncIOxUGXQJ8-cx87vLikZiHOTNGQEInp3nb_CeFw7y2yuJoZumpau3DBUx5V1/s812/Win11Installer10.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDKU4bzc8PYfvrRNVgoQ038q5T9ZVFg2ebpZKjOdwh7njqrTQcp1jEsS3KmGgLqloBInsW-WJu3coySNApuL4JocTbTH6UcridWunnAqF_wPKt0t7OQfpoDc4SY8EncIOxUGXQJ8-cx87vLikZiHOTNGQEInp3nb_CeFw7y2yuJoZumpau3DBUx5V1/s812/Win11Installer10.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEja31VpoXAyUeinucn0m8ug0f-aLRYzvOukM5hjMDsK1l8oStduZ_RavW6v-ciP09VFnmLg0ZCXXXuajWpS5TudHAR0lIigsKno9sZQ_wi1rDA-IfZ-LGkDUDPTbTTrz2pWXDDhfYyxlYP-P8NYjs1EAEMLJgsESmjKOMVDygagXwg548wQwnzh1Lvs/s812/Win11Installer12.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEja31VpoXAyUeinucn0m8ug0f-aLRYzvOukM5hjMDsK1l8oStduZ_RavW6v-ciP09VFnmLg0ZCXXXuajWpS5TudHAR0lIigsKno9sZQ_wi1rDA-IfZ-LGkDUDPTbTTrz2pWXDDhfYyxlYP-P8NYjs1EAEMLJgsESmjKOMVDygagXwg548wQwnzh1Lvs/s812/Win11Installer12.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoIYyfVpPWVNmHFkx7aXVfBKjOhhfI0cEMZJD8ZuWRTjMxk5phUD6FvyQT3_vYAkp11U2zDrrXTIr2so5Nxcerx7F9r5xvCSslXd-WpxFvTKLu5MymZC1B1VeJxSSzic4VWfNELkh0xnrPdC08QnPNY3CwU1O_AZkPcC3OsJBOjl4hrLTzE8zNFkZf/s812/Win11Installer13.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoIYyfVpPWVNmHFkx7aXVfBKjOhhfI0cEMZJD8ZuWRTjMxk5phUD6FvyQT3_vYAkp11U2zDrrXTIr2so5Nxcerx7F9r5xvCSslXd-WpxFvTKLu5MymZC1B1VeJxSSzic4VWfNELkh0xnrPdC08QnPNY3CwU1O_AZkPcC3OsJBOjl4hrLTzE8zNFkZf/s812/Win11Installer13.png" width="30%"></a></p>
<h3
id="残りのvirtioドライバーをインストール">残りのvirtioドライバーをインストール</h3>
<p>Windowsセットアップ完了後、Windows上でE:ドライブ(virtio-win-0.1.229)を開きます。そこにあるインストーラを起動し、virtioドライバをインストールしておきます。</p>
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgR_4IR-LHHigsJUqb__6_-AmY3GL1wfXo8G9-lsj7BdPzbkmox22bTZBYAwiuBxTLfdyc0kfg-ny2MhAlQtCLwQ8DKgu0N-l4bC8ftMjqemOkWrUQqp3bziniBOPh6ff1D8ssezNP4YfE9rr6d0V8Z8zxahcrHTvtU6SEAARKw-9N3p0vJk3o5ctUl/s812/virtio_install01.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgR_4IR-LHHigsJUqb__6_-AmY3GL1wfXo8G9-lsj7BdPzbkmox22bTZBYAwiuBxTLfdyc0kfg-ny2MhAlQtCLwQ8DKgu0N-l4bC8ftMjqemOkWrUQqp3bziniBOPh6ff1D8ssezNP4YfE9rr6d0V8Z8zxahcrHTvtU6SEAARKw-9N3p0vJk3o5ctUl/s812/virtio_install01.png" width="30%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjni-rgb1JVtBCy-NMiEReK7pe5TW3w3Ecc3gzmRUHqweir9ebthWDOpS11vTSf3W3ekwB4wpoB-Tl7LCTR-3n-w4XhLxgnfDsPeGycsefskDmLGXXiN4WNTTcrzDTyTjH7Z_oS7plt5aSodaBvw8CRTOo_gbAWEdHp_8ZCmE58lAbCHAruZuPdC1Rn/s812/virtio_install02.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjni-rgb1JVtBCy-NMiEReK7pe5TW3w3Ecc3gzmRUHqweir9ebthWDOpS11vTSf3W3ekwB4wpoB-Tl7LCTR-3n-w4XhLxgnfDsPeGycsefskDmLGXXiN4WNTTcrzDTyTjH7Z_oS7plt5aSodaBvw8CRTOo_gbAWEdHp_8ZCmE58lAbCHAruZuPdC1Rn/s812/virtio_install02.png" width="30%"></a></p>
<p>以上で、ネットワーク関連は未設定状態ですが、Windows11がインストールされたqcow2ディスクイメージ、win11pro.qcow2ができあがります。</p>
<h2 id="普段使いのために">普段使いのために</h2>
<p>Windows11のインストールは、ネットワーク無しで行った。仮想マシン上のWindows11を利用するには、ネットワークが使えないと困るでしょう。
ネットワークを使えるようにするためには、次の3つの準備が必要です。</p>
<ol type="1">
<li>Linuxホスト上でのネットワーク設定</li>
<li>KVM起動コマンドをネットワークが使えるように修正</li>
<li>Windows11ゲストマシン上でネットワーク設定</li>
</ol>
<h3
id="linuxホストのネットワークセットアップ">Linuxホストのネットワークセットアップ</h3>
<p>Linuxホスト上では、以下の設定を行えば十分です。</p>
<pre><code>brdg=kbr0
outif=wlan0
addr=10.0.0.254/24
# ブリッジインターフェースの準備
brctl addbr $brdg
ip add add dev $brdg $addr
ip link set dev $brdg up
# IPマスカレード設定
iptables -t nat -A POSTROUTING -s $addr -o $outif -j MASQUERADE
# IPフォワーディング許可
echo 1 > /proc/sys/net/ipv4/conf/$outif/forwarding
echo 1 > /proc/sys/net/ipv4/conf/$brdg/forwarding</code></pre>
<p>念の為、上記で用意したものをもとに戻すのは、以下のやり方で良いでしょう。</p>
<pre><code># IPフォワーディング許可を取り消す
echo 0 > /proc/sys/net/ipv4/conf/$outif/forwarding
echo 0 > /proc/sys/net/ipv4/conf/$brdg/forwarding
# ブリッジインターフェースを消す
ip link set dev $brdg down
brctl delbr $brdg
# IPマスカレード設定を消す
iptables -t nat -D POSTROUTING -s $addr -o $outif -j MASQUERADE</code></pre>
<h3 id="windowsゲスト起動コマンド">Windowsゲスト起動コマンド</h3>
<p>qemuコマンドはオプションが多いのでスクリプト化しておくと良いでしょう。</p>
<pre><code>run.win11.sh:
#!/bin/bash
sudo qemu-system-x86_64 \
-m 8192 -cpu host \
-smp 6,sockets=1,dies=1,cores=6,threads=1 \
-drive file=./win11pro.qcow2,if=virtio,format=qcow2,discard=unmap \
-display spice-app \
-machine q35,accel=kvm \
-rtc base=localtime,clock=host \
-drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE_4M.ms.fd,readonly=on \
-drive if=pflash,format=raw,file=./OVMF_VARS_4M.ms.fd \
-tpmdev passthrough,id=tpm0,path=/dev/tpm0,cancel-path=/dev/null \
-device tpm-tis,tpmdev=tpm0 \
-device virtio-net-pci,netdev=dev1,mac=52:54:00:11:00:12,id=net1 \
-netdev tap,id=dev1,vhost=on,script=./qemu-ifup</code></pre>
<p>仮想マシン起動時にデバイスをホストのブリッジにアタッチします。</p>
<pre><code>qemu-ifup:
#!/bin/sh
bridge=kbr0
/sbin/ip link set dev $1 up promisc off
/sbin/brctl addif $bridge $1</code></pre>
<h3
id="windowsゲストのネットワーク設定">Windowsゲストのネットワーク設定</h3>
<p>次のアドレスを設定</p>
<pre><code>IPv4アドレス: 10.0.0.1/24
ゲートウェイ: 10.0.0.254
DNSサーバ: 192.168.40.1(Linuxホストと同じ設定にすると良いと思う。)</code></pre>
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5VRIXuwnZ1FiZHXeG2lmJDIVphJlT4sqLOKeOK3Sk4eXW-B4r1P_FsjfEq6jvwlFcWtgoMYbGmD2-EFBAPEkBzomgXZH8tuaYG_soLx81SkayPRt-3Vi4fyR0695IlxK3ifT7EmEnI-GWbQC3JWYBAnSjJOek_ZOM2o7ku5m0J-p3nQ3OZOseOxes/s812/network_setup01.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5VRIXuwnZ1FiZHXeG2lmJDIVphJlT4sqLOKeOK3Sk4eXW-B4r1P_FsjfEq6jvwlFcWtgoMYbGmD2-EFBAPEkBzomgXZH8tuaYG_soLx81SkayPRt-3Vi4fyR0695IlxK3ifT7EmEnI-GWbQC3JWYBAnSjJOek_ZOM2o7ku5m0J-p3nQ3OZOseOxes/s812/network_setup01.png" width="30%"></a></p>
<p>この他に、Windows 11 Proの場合はRemote
Desktop機能があるので、利用可能にしておくと良いでしょう。</p>
<h2 id="その他のtips">その他のTips</h2>
<ul>
<li>一旦ビューワーを閉じたあともう一度接続するには</li>
</ul>
<pre><code>$ sudo virt-viewer -c spice+unix:///tmp/.JVB811/spice.sock</code></pre>
<ul>
<li>Remote Desktop接続は、例えば次のようにする</li>
</ul>
<pre><code>user=ktaka
xfreerdp /u:$user /size:1900x1000 +fonts +clipboard /audio-mode:1 /v:10.0.0.1</code></pre>
<ul>
<li>qemu起動時にはwindowを表示せず、rdpのみで使うには
<code>--display spice-app</code>を<code>--display none</code>にすれば良い。</li>
</ul>
<h2 id="参考文献">参考文献</h2>
<h3 id="qemuでのtpmの使い方">QemuでのTPMの使い方</h3>
<p>以下のドキュメントに十分な情報があります。</p>
<ul>
<li><a href="https://qemu-project.gitlab.io/qemu/specs/tpm.html">QEMU
TPM Device</a></li>
</ul>
<h3 id="qemuでのuefi-secure-bootのやり方">QEMUでのUEFI Secure
Bootのやり方</h3>
<ul>
<li><a
href="https://wiki.debian.org/SecureBoot/VirtualMachine">SecureBootVirtualMachine</a></li>
<li><a
href="https://www.labbott.name/blog/2016/09/15/secure-ish-boot-with-qemu/">Secure(ish)
boot with QEMU</a></li>
</ul>
<h3 id="どのuefi-firmwareを使うべきか">どのUEFI
firmwareを使うべきか</h3>
<p>Debianのドキュメントに書いてあります。</p>
<ul>
<li>Secure Boot
pre-enabledな、OVMF_CODE_4M.ms.fdとOVMF_VARS_4M.ms.fdをセットで使う。</li>
<li>前者はRead Onlyで、後者はコピーしたものをRead
Write可能な状態で利用する。</li>
<li>UEFIのメニューでSaveした設定変更は、OVMF_VARS_4M.ms.fdのコピーに書き込まれる。</li>
</ul>
<p>/usr/share/doc/ovmf/README.Debian</p>
<pre><code>The OVMF_CODE*.fd files provide UEFI firmware for a QEMU guest that is
intended to be read-only. The OVMF_VARS*.fd files provide UEFI variable
template images which are intended to be read-write, and therefore each
guest should be given its own copy. Here's an overview of each of them:
OVMF_CODE_4M.fd
Use this for booting guests in non-Secure Boot mode. While this image
technically supports Secure Boot, it does so without requiring SMM
support from QEMU, so it is less secure. Use the OVMF_VARS.fd template
with this.
OVMF_CODE_4M.ms.fd
This is a symlink to OVMF_CODE_4M.secboot.fd. It is useful in the context
of libvirt because the included JSON firmware descriptors will tell libvirt
to pair OVMF_VARS.ms.fd with it, which has Secure Boot pre-enabled.
OVMF_CODE_4M.secboot.fd
Like OVMF_CODE_4M.fd, but will abort if QEMU does not support SMM.
Use this for guests for which you may enable Secure Boot. Be aware
that the included JSON firmware descriptors associate this with
OVMF_CODE_4M.fd. Which means, if you specify this image in libvirt, you'll
get a guest that is Secure Boot-*capable*, but has Secure Boot disabled.
To enable it, you'll need to manually import PK/KEK/DB keys and activate
Secure Boot from the UEFI setup menu. If you want Secure Boot active from
the start, consider using OVMF_CODE.ms.fd instead.
OVMF_VARS_4M.fd
This is an empty variable store template, which means it has no
built-in Secure Boot keys and Secure Boot is disabled. You can use
it with any OVMF_CODE image, but keep in mind that if you want to
boot in Secure Boot mode, you will have to enable it manually.
OVMF_VARS_4M.ms.fd
This template has distribution-specific PK and KEK1 keys, and
the default Microsoft keys in KEK/DB. It also has Secure Boot
already activated. Using this with OVMF_CODE.ms.fd will boot a
guest directly in Secure Boot mode.
OVMF32_CODE_4M.secboot.fd
OVMF32_VARS_4M.fd
These images are the same as their "OVMF" variants, but for 32-bit guests.
OVMF_CODE.fd
OVMF_CODE.ms.fd
OVMF_CODE.secboot.fd
OVMF_VARS.fd
OVMF_VARS.ms.fd
These images are the same as their "4M" variants, but for use with guests
using a 2MB flash device. 2MB flash is no longer considered sufficient for
use with Secure Boot. This is provided only for backwards compatibility.
OVMF_VARS_4M.snakeoil.fd
This image is **for testing purposes only**. It includes an insecure
"snakeoil" key in PK, KEK & DB. The private key and cert are also
shipped in this package as well, so that testers can easily sign
binaries that will be considered valid. Intended for use with
OVMF_CODE_4M.secboot.fd.
PkKek-1-snakeoil.key
PkKek-1-snakeoil.pem
The private key and certificate for the snakeoil key. Use these
to sign binaries that can be verified by the key in the
OVMF_VARS.snakeoil.fd template. The password for the key is
'snakeoil'.
-- dann frazier <dannf@debian.org>, Fri, 11 Dec 2020 17:30:59 -0700</code></pre>
<h2 id="まとめ">まとめ</h2>
<p>QEMUコマンドのみでKVM仮想マシンを起動し、Windows11をインストールする方法についてまとめました。
Window10の場合との違いは、TPM及びUEFI Secure Bootが必須であることです。
Windows11の場合も、GUIプログラムvirt-managerでのインストール方法はネット上でよく見かけますが、qemuコマンドのみでのやり方はあまり多くないようです。
余計なものはなるべくインストールしたくない人、ソフトウェアスタックをミニマムに保って中身を理解しながら使いたい人の役に立てば幸いです。</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-61765709538433525742023-03-21T04:42:00.057+09:002024-01-26T18:22:58.314+09:00KVM再入門:Qemuコマンドラインで仮想マシンを起動し、Windows10ゲストをセットアップする。<h1 id="目次">目次</h1>
<ul>
<li><a href="#目次">目次</a>
<ul>
<li><a href="#はじめに">はじめに</a></li>
<li><a href="#windows10インストール">Windows10インストール</a>
<ul>
<li><a href="#事前準備">事前準備</a></li>
<li><a
href="#仮想マシンの起動とwindowsのインストール">仮想マシンの起動とWindowsのインストール</a></li>
</ul></li>
<li><a href="#仮想マシンの利用">仮想マシンの利用</a>
<ul>
<li><a href="#ライセンス認証">ライセンス認証</a></li>
<li><a
href="#ホストlinuxのネットワーク設定">ホストLinuxのネットワーク設定</a></li>
<li><a
href="#仮想マシンの起動コマンドライン">仮想マシンの起動コマンドライン</a></li>
</ul></li>
<li><a href="#まとめ">まとめ</a></li>
</ul></li>
</ul>
<h2 id="はじめに">はじめに</h2>
<p>最近Debian
Linuxをインストールしたノートパソコン上に、KVM仮想マシンのゲストOSとして、Windows10をインストールする機会がありました。
はじめはGUI形式のvirt-managerをつかって、ボタンをポチポチ押しながらゲストOSをインストールしていました。
しかし、調べていくうちに、qemuコマンドラインのみで仮想マシンを起動しWindowsをインストールする方法にたどり着いたので、それについてまとめておこうと思います。</p>
<p>virt-managerは非常に便利ですが、libvirtやそれが依存する数多くのパッケージをインストールし、libvirtdなどをデーモンとして動かしておかなければなりません。
qemuコマンドラインのみでWindowsをインストールする方法をおさえておけば、本来不必要だったものをインストールしなくて済みますし、構成がシンプルであるため動作の仕組みが容易に理解でき、なにかトラブルがあった場合にも比較的容易にデバッグが可能になると期待できます。</p>
<h2 id="windows10インストール">Windows10インストール</h2>
<h3 id="事前準備">事前準備</h3>
<h4
id="インストールメディアvirtioドライバのダウンロード">インストールメディア、virtioドライバのダウンロード</h4>
<p>まず、あらかじめ必要なものをダウンロードしておきます。</p>
<ul>
<li>Windows10のインストールメディアWin10_22H2_Japanese_x64.isoを、<a
href="https://www.microsoft.com/ja-jp/software-download/windows10ISO">Microsoftのページ</a>からダウンロードする。</li>
<li>virtioドライバをが必要になるので、<a
href="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/">Fedoraのページ</a>からダウンロードします。今回は、この記事の執筆時点で最新のvirtio-win-0.1.229.isoを利用しました。</li>
</ul>
<h4
id="必要パッケジージのインストール">必要パッケジージのインストール</h4>
<pre><code>sudo apt-get install qemu-system-x86 virt-viewer</code></pre>
<h4
id="インストール先ドライブのイメージファイル作成">インストール先ドライブのイメージファイル作成</h4>
<p>Windowsのインストール先として、40Gbyteのqcow2イメージファイルを作成します。</p>
<pre><code>qemu-img create -f qcow2 win10.qcow2 40G</code></pre>
<h4 id="不具合回避">不具合回避</h4>
<p>後述のコマンドラインで仮想マシンを起動しようとすると、以下のようなエラーメッセージが出て、仮想マシンが起動できない。</p>
<pre><code>qemu-system-x86_64: info: Launching display with URI: spice+unix:///tmp/.BHI811/spice.sock
qemu-system-x86_64: Failed to launch spice+unix:///tmp/.BHI811/spice.sock URI: Operation not supported
qemu-system-x86_64: You need a capable Spice client, such as virt-viewer 8.0</code></pre>
<p>これを回避するために/root/.config/mimeapps.listに以下の行を追加する。</p>
<pre><code>[Added Associations]
x-scheme-handler/spice+unix=remote-viewer.desktop</code></pre>
<h3
id="仮想マシンの起動とwindowsのインストール">仮想マシンの起動とWindowsのインストール</h3>
<p>以下のコマンドで仮想マシンを起動すると、Windowsのインストーラーウィンドウが現れますので、画面の指示に従ってインストールを進めます。</p>
<pre><code>sudo qemu-system-x86_64 \
-machine q35,accel=kvm \
-m 8192 -cpu host \
-smp 6,sockets=1,dies=1,cores=6,threads=1 \
-display spice-app \
-rtc base=localtime,clock=host \
-drive file=./win10pro.qcow2,if=virtio,format=qcow2,discard=unmap \
-drive file=./Win10_22H2_Japanese_x64.iso,index=0,media=cdrom \
-drive file=./virtio-win-0.1.229.iso,index=1,media=cdrom
</code></pre>
<ul>
<li>最初はインストール先のドライブが見えないですが、ドライバーの読み込み→E:ドライブ→amd64→win10ディレクトリを選択しOKを押すと、virtioドライバが読み込まれ、ドライブ0が見えるようになります。</li>
<li>Windows10のインストールメディアにはいくつかのバージョンのWindowsが入っており、プロダクトキーの入力をスキップすると、インストールするバージョンを選択できるようになります。HomeかProかをそこで選択します。</li>
<li>Windowsのインストールを進めると、Microsoftアカウントへのサインインを求められますが、ローカルアカウントを作成し次に進めます。</li>
<li>ローカルアカウント作成の選択肢が提示されない場合は、メールアドレスにno@thankyou.com、パスワードに適当な文字列を入力するといったんサインインに失敗し、ローカルアカウント作成ができるようになるようです。</li>
<li>上記の仮想マシン起動コマンドではネットワークの設定を行っていないので、いずれにせよWindowsサインインは行われないはずです。</li>
<li>Windowsセットアップ完了後、Windows上でE:ドライブ(virtio-win-0.1.229)を開きます。そこにあるインストーラを起動し、virtioドライバをインストールしておきます。</li>
</ul>
<p>以上で、Windows10がインストールされたKVM用のqcow2イメージができあがります。</p>
<p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMNDMl1ktluDNjThzpRU_ofKyib4MDPEpTyaIzR4p0xDpUGUhSzWvJ17_Sy7iUQeGjOgrVcK1V2UiqdOtv6FGFuEzPreRMYHNN6wbQI3T9kfcWkXccRZd3HTCiKPyRWY-_8wSomn2aBSxXwVUgPUw17-bQnBklLsVP5tJe4clUH0UzHBbRw5IboxqB/s1920/KVM_Win10pro01.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMNDMl1ktluDNjThzpRU_ofKyib4MDPEpTyaIzR4p0xDpUGUhSzWvJ17_Sy7iUQeGjOgrVcK1V2UiqdOtv6FGFuEzPreRMYHNN6wbQI3T9kfcWkXccRZd3HTCiKPyRWY-_8wSomn2aBSxXwVUgPUw17-bQnBklLsVP5tJe4clUH0UzHBbRw5IboxqB/s1920/KVM_Win10pro01.png" width="45%"></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPv7yX7d5eWuv_mNzsb2bCXsLl0FoWXVCDD9HwnKzNXfloJT_pk_ZPxBnOX26uOxFSKwf9QL4MXVX-O0yVE1VGXuPc71nohgm8PkFm3jPWwvq_OnJwah-EDUMPjoRlq_pqXjkTVQnFSCaEXbXLwiczgI9mXTLuwCbIv3dJAllPddU4KaZCzOVErbpO/s1920/KVM_Win10pro02.png" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPv7yX7d5eWuv_mNzsb2bCXsLl0FoWXVCDD9HwnKzNXfloJT_pk_ZPxBnOX26uOxFSKwf9QL4MXVX-O0yVE1VGXuPc71nohgm8PkFm3jPWwvq_OnJwah-EDUMPjoRlq_pqXjkTVQnFSCaEXbXLwiczgI9mXTLuwCbIv3dJAllPddU4KaZCzOVErbpO/s1920/KVM_Win10pro02.png" width="45%"></a></p>
<h2 id="仮想マシンの利用">仮想マシンの利用</h2>
<h3 id="ライセンス認証">ライセンス認証</h3>
<p>基本的に仮想マシン一台ごとにライセンスが必要ですので、仮想マシンのWindows上でプロダクトキーを入力し、ライセンス認証を行います。Windowsを消し、Linuxをインストールしたノートパソコン上では、プリインストールしてあったWindowsのプロダクトキーが利用可能であると思われます。しかし、あまり不正確なことはここに書けないので参考にしたリンクのみを以下に示します。</p>
<ul>
<li>http://blog.yottun8.com/archives/794</li>
<li>https://orebibou.com/ja/home/201905/20190527_001/
<ul>
<li>https://superuser.com/questions/1313241/install-windows-10-from-an-unbooted-oem-drive-into-virtualbox</li>
<li>https://gist.github.com/Informatic/49bd034d43e054bd1d8d4fec38c305ec</li>
</ul></li>
</ul>
<h3
id="ホストlinuxのネットワーク設定">ホストLinuxのネットワーク設定</h3>
<p>仮想マシンのネットワーク接続についてはいくつもやり方があります。私の場合、ゲストOSがWindowsであるかLinuxにあるかに関わらず、以下の方式を好んで利用しています。</p>
<ul>
<li>仮想マシン用のブリッジを予め作成しておき、仮想マシンの起動時にそこにアタッチする。</li>
<li>外部へはSNAT(MASQUERADE)で接続する。</li>
<li>ネットワークアドレスは、10.0.0.0/24を利用する。</li>
<li>仮想マシン上ではその帯域の固定IPを利用する。</li>
<li>DNSサーバは、ホストOSが使っているのと同じもの利用する。</li>
</ul>
<p>ブリッジインターフェースの作成は、Linuxのネットワーク設定ファイルに書いておき、ノートパソコン起動時に自動的に設定が行われます。</p>
<pre><code>/etc/network/interfaces.d/kbr0
auto kbr0
iface kbr0 inet static
bridge_ports none
bridge_fd 0
bridge_maxwait 0
address 10.0.0.254
netmask 255.255.255.0
up /etc/network/masquerade.sh</code></pre>
<p>iptables natの設定</p>
<pre><code>/etc/network/masquerade.sh
#!/bin/bash
if ! iptables -t nat -C POSTROUTING -o eth0 -j MASQUERADE > /dev/null 2>&1 ; then
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
fi
</code></pre>
<p>仮想マシン起動時に仮想マシンのネットワークインターフェースをホストLinuxのブリッジにアタッチするためのスクリプトも用意しておきます。</p>
<pre><code>/etc/network/qemu-ifup
#!/bin/sh
bridge=kbr0
/sbin/ip link set dev $1 up promisc off
/sbin/brctl addif $bridge $1</code></pre>
<h3
id="仮想マシンの起動コマンドライン">仮想マシンの起動コマンドライン</h3>
<p>ネットワーク接続込のコマンドライン。</p>
<pre><code>sudo qemu-system-x86_64 \
-machine q35,accel=kvm \
-m 8192 -cpu host \
-smp 6,sockets=1,dies=1,cores=6,threads=1 \
-display spice-app \
-rtc base=localtime,clock=host \
-drive file=./win10pro.qcow2,if=virtio,format=qcow2,discard=unmap \
-device virtio-net-pci,netdev=dev1,mac=52:54:00:11:00:12,id=net1 \
-netdev tap,id=dev1,vhost=on,script=/etc/network/qemu-ifup</code></pre>
<p>そして、仮想マシンの起動後、Windows上でIPアドレス、DNSサーバー等の設定を行います。</p>
<h2 id="まとめ">まとめ</h2>
<p>QEMUコマンドのみでKVM仮想マシンを起動し、Windows10をインストールする方法についてまとめました。
ネット上にはvirshコマンドラインや、virt-managerのGUIでのインストール方法はよく見かけますが、qemuコマンドのみでのやり方はあまり多くないようです。
余計なものはなるべくインストールしたくない人、ソフトウェアスタックをミニマムにして中身を理解しながら使いたい人のお役に立てば幸いです。</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-64554641691948843742021-01-22T13:35:00.005+09:002021-01-22T13:40:43.435+09:00社外からSOCKS5プロキシ経由でRemoteDesktop接続<p>SOCKS5トンネル掘り </p>
hirakegoma.sh
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">#!/bin/bash
while true; do
echo "Connecting gw.example.com:22 ... "
ssh -4ND 18888 gw.example.com -p 22
sleep 1
echo retrying
done
</pre></div><div><br /></div><div>xfreerdpのラッパースクリプト</div><div>社内(≈名前解決可能)なら直接、そうでないならSOCKS5プロキシ経由でRDP接続する。</div><div><br /></div>rdp.sh<br /><!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">#!/bin/bash
case "$1" in
host1)
user=user1
host=host1.inside.example.com
;;
host2)
user=user2
host=host2.inside.example.com
;;
*)
echo Usage $1 host_nick_name
exit
esac
if host $host ; then
echo Connecting $user@$host
xfreerdp /u:$user /size:1400x900 /v:$host
else
echo Connecting $user@$host via SOCKS proxy
xfreerdp /u:$user /size:1400x900 /v:$host /proxy:socks5://localhost:18888
fi
</pre></div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com2tag:blogger.com,1999:blog-4279971486311743746.post-48869802473307368952021-01-22T03:45:00.003+09:002021-01-22T03:46:45.506+09:00SSH踏み台サーバを経由して社内サーバにアクセスする最近のリモートワークの状況下で自宅から職場のサーバにアクセスしなければならないことも多いと思います。<div><br /></div><div>VPNを使ったり、OAuth付きのproxyサーバを立てたり、いろいろとやり方があると思います。今回はSSHの踏み台サーバ経由で職場内のWEBサーバにアクセスする方法について、やり方を記録して置こうと思います。</div><div><br /></div><div>昔から、1. SSHのダイナミックフォワードでトンネルを掘る、2. ブラウザにproxy.pacを読ませて特定の(社内サーバ)URLのみにそのトンネルを使わせる、といった方法は利用していましたが、あるバージョンを境にChromeでproxy.pacファイルを読まなくなってしまい、放置していました。</div><div><br /></div><div>今回改めてやり方を調べてみたというのが背景です。</div><div><br /></div><div>トンネル掘り</div><div>トンネルにはSSHのダイナミックフォワードを使います。例えば以下のような感じ。</div><div><br /></div>
hirakegoma.sh
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">#!/bin/bash
while true; do
echo "Connecting gw.example.com:22 ... "
ssh -4ND 18888 gw.example.com -p 22
sleep 1
echo retrying
done
</pre></div>
<div><br /></div><div>職場内のサーバ、xxx.inside.example.comへのアクセスを上で作ったトンネルを経由させるには、例えば以下のようにすれば良い。</div><div><br /></div><div>proxy.pac</div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">function FindProxyForURL(url, host)
{
if (dnsDomainIs(host, ".inside.example.com"))
if (isResolvable(host))
return "DIRECT";
else
return "SOCKS5 localhost:18888";
else
return "DIRECT";
}
</pre></div>
<div><br /></div><div>会社内ではサーバ名前解決ができるであろうから、名前ができないときのみトンネルを通すようになっている。</div><div><br /></div><div>Chrome起動オプション</div><div><br /></div><div>Chromeでproxy.pacのありかを指定するには、システムの環境設定でやればいいらしい。残念なことにLinuxの場合にはそういうわけにもいかず、Chrome起動時にオプションを引数として渡してあげる必要がある。</div><div><br /></div><div>以前は以下のような感じでうまく行っていた。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">google-chrome %U --proxy-pac-url=file:///home/ktaka/proxy.pac
</pre></div><div><br /></div>
最近は"--proxy-pac-url=f"でファイルを指定しても無視されてしまうようなので、<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=839566#c22" rel="nofollow" target="_blank">ワークアラウンド</a>が必要である。<div><br /></div><div>結局行き着いたのが以下のオプション。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">google-chrome %U --proxy-pac-url='data:application/x-javascript-config;base64,'$(base64 -w0 /home/me/proxy.pac)
</pre></div>
<div><br /></div><div>最近のサーバやワークステーションは、BMCというミニプロセッサが載っていて、Webブラウザー経由で電源のオンオフ、ハードリセット、BIOS設定の変更などができます。</div><div><br /></div><div>今回のような簡易プロキシを使うことによって、これらの職場内にあるサーバに、外部から簡単にアクセスすることができるようになります。</div><div><br /></div><div>BMC搭載の高性能なワークステーションはこちらをどうぞ→<a href="https://ccmp.jp/hardware/workstation/373-amd-threadripper.html">AMD Ryzen™ Threadripper™ ECCメモリ IPMI搭載 ワークステーション</a></div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-14032044418868988272021-01-17T21:49:00.004+09:002021-01-17T23:57:26.268+09:00Debian Linuxのノートパソコンでハイバネート<p> <span face="Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif" style="background-color: white; font-size: 13px;">Linuxカーネルには、スワップ領域にメモリの内容を書き出してハイバネートする機能がある。 必要な要件等は以下の通り。</span></p><ul style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;"><li>十分な大きさのスワップデバイスまたはファイルを用意する。(メモリ容量と同じくらいあれば良いが、そこまでなくても良いという情報もある。)</li><li>カーネル起動オプションresume=デバイス名を加えカーネルを起動。</li><li>uswsuspを利用することもできるが、利用しなくても良い。その場合initrdをいじる必要は無い。</li></ul><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">参考文献</p><ul style="background-color: white;"><li><span face="Verdana, Arial, Bitstream Vera Sans, Helvetica, sans-serif"><span style="font-size: 13px;"><a href="https://wiki.archlinux.jp/index.php/%E3%82%B5%E3%82%B9%E3%83%9A%E3%83%B3%E3%83%89%E3%81%A8%E3%83%8F%E3%82%A4%E3%83%90%E3%83%8D%E3%83%BC%E3%83%88" target="_blank">https://wiki.archlinux.jp/index.php/サスペンドとハイバネート</a></span></span></li><li><span face="Verdana, Arial, Bitstream Vera Sans, Helvetica, sans-serif"><span style="font-size: 13px;"><a href="https://www.kernel.org/doc/Documentation/power/states.txt" target="_blank">https://www.kernel.org/doc/Documentation/power/states.txt</a></span></span></li><li><span face="Verdana, Arial, Bitstream Vera Sans, Helvetica, sans-serif"><span style="font-size: 13px;"><a href="https://www.kernel.org/doc/html/latest/power/basic-pm-debugging.html" target="_blank">https://www.kernel.org/doc/html/latest/power/basic-pm-debugging.html</a></span></span></li></ul><div><span face="Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif" style="background-color: white; font-size: 13px;">以下、実際の設定</span></div><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">/etc/fstab (該当部分のみ)</p><pre class="wiki" style="background: rgb(247, 247, 247); border-radius: 0.3em; border: 1px solid rgb(215, 215, 215); box-shadow: rgb(238, 238, 238) 0px 0px 1em; font-size: 13px; margin: 1em 1.75em; overflow: auto; padding: 0.25em;">#/dev/nvme0n1p7 swap swap defaults 0 0
/swapfile swap swap defaults 0 0
</pre><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">デバイスをスワップとして使う場合と、ファイル(/swapfile)をスワップとして使う場合とで必要なカーネルパラメータが異なる。 後者の場合はresume_offsetも必要。オフセット値は以下のように確認可能。この場合86093824がオフセット値。</p><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">sudo filefrag -v /swapfile</p><pre class="wiki" style="background: rgb(247, 247, 247); border-radius: 0.3em; border: 1px solid rgb(215, 215, 215); box-shadow: rgb(238, 238, 238) 0px 0px 1em; font-size: 13px; margin: 1em 1.75em; overflow: auto; padding: 0.25em;">Filesystem type is: ef53
File size of /swapfile is 17179869184 (4194304 blocks of 4096 bytes)
ext: logical_offset: physical_offset: length: expected: flags:
0: 0.. 2047: 86093824.. 86095871: 2048:
1: 2048.. 4095: 86167552.. 86169599: 2048: 86095872:
2: 4096.. 8191: 86155264.. 86159359: 4096: 86169600:
以下略
</pre><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">/boot/grub/grub.cfg (該当部分のみ)</p><pre class="wiki" style="background: rgb(247, 247, 247); border-radius: 0.3em; border: 1px solid rgb(215, 215, 215); box-shadow: rgb(238, 238, 238) 0px 0px 1em; font-size: 13px; margin: 1em 1.75em; overflow: auto; padding: 0.25em;">menuentry "Debian Linux resume-test1" {
linux /boot/vmlinuz root=/dev/nvme0n1p6 vga=0x305 panic=10 net.ifnames=0 biosdevname=0 resume=/dev/nvme0n1p7
}
menuentry "Debian Linux resume-test2" {
linux /boot/vmlinuz root=/dev/nvme0n1p6 vga=0x305 panic=10 net.ifnames=0 biosdevname=0 resume=/dev/nvme0n1p6 resume_offset=86093824
}
</pre><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">ノートパソコンのディスプレイを閉じたときにhibernateする設定</p><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">/etc/acpi/events/lid_down</p><pre class="wiki" style="background: rgb(247, 247, 247); border-radius: 0.3em; border: 1px solid rgb(215, 215, 215); box-shadow: rgb(238, 238, 238) 0px 0px 1em; font-size: 13px; margin: 1em 1.75em; overflow: auto; padding: 0.25em;">event=button[ /]lid
action=/etc/acpi/lid_down.sh
</pre><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">/etc/acpi/lid_down.sh</p><pre class="wiki" style="background: rgb(247, 247, 247); border-radius: 0.3em; border: 1px solid rgb(215, 215, 215); box-shadow: rgb(238, 238, 238) 0px 0px 1em; font-size: 13px; margin: 1em 1.75em; overflow: auto; padding: 0.25em;">#!/bin/sh
grep -q closed /proc/acpi/button/lid/*/state
if [ $? -eq 0 ]
then
echo platform > /sys/power/disk ; echo disk > /sys/power/state
fi
exit 0
</pre><p style="background-color: white; font-family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif; font-size: 13px;">以上で、ハイバネートできることを確認した。</p>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-37670683935031339382021-01-15T20:02:00.001+09:002021-06-11T15:04:25.947+09:00Dynabook GZ83/MLにDebian Linuxをのせて使ってます。<div class="separator" style="clear: both; text-align: left;">先日購入したノートパソコンのご紹介です。</div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">ちまたではM1 Macbook Air/Proの評判が高いようですが、920gの12インチMacbookが出なかったことに落胆し、軽量なノートパソコンを購入してしまいました。</div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">M1 Macの性能がものすごく高いようなので、失敗したかなと思いつつも、でもAirといっても1.3kgもあったら持って歩けないよなぁとか、やっぱり悩みつつもLinuxを日頃から触るのは楽しいよなぁとか、自分に言い聞かせながら悶々とした日々を過ごしております。</div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">Macbook Airとても良いんですけどね、重くなければ。</div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">そして購入したノートパソコンがこれ。Dynabook GZ83/ML。<a href="https://dynabook.com/direct/pc/catalog/2020-spring/gz83m/spec.html">https://dynabook.com/direct/pc/catalog/2020-spring/gz83m/spec.html</a></div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9oeSyKfwE71gpq6LVSwTQHrOAue1UX0Hx-Yw2cqFOhfatY7mIBdrWVd66udjA5mgcA4l4Ka33MVUXskEstJTVSGXX_IvP9t9NK17bjoHHNS3WFMDpSAxo5sOh32XqzIdee9-Fhh1kb_Y/s1600/1610706729565933-0.png" style="margin-left: 1em; margin-right: 1em;">
<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9oeSyKfwE71gpq6LVSwTQHrOAue1UX0Hx-Yw2cqFOhfatY7mIBdrWVd66udjA5mgcA4l4Ka33MVUXskEstJTVSGXX_IvP9t9NK17bjoHHNS3WFMDpSAxo5sOh32XqzIdee9-Fhh1kb_Y/s1600/1610706729565933-0.png" width="400" />
</a>
</div><div><div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5u5WNoQ9uW9-f1C9Zu18WVMxe2Rye2cQlYaAfOpksvRcW5VwFMD49hExJHIILR0DJEDMTgCcX5_b8V8NmB9fCHHBcG1lNdOBdpsBuGpebUg_sL1t4VT56F_tQYeSXxK2sfavckJXb-gs/s1600/1610706724492470-1.png" style="margin-left: 1em; margin-right: 1em;">
<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5u5WNoQ9uW9-f1C9Zu18WVMxe2Rye2cQlYaAfOpksvRcW5VwFMD49hExJHIILR0DJEDMTgCcX5_b8V8NmB9fCHHBcG1lNdOBdpsBuGpebUg_sL1t4VT56F_tQYeSXxK2sfavckJXb-gs/s1600/1610706724492470-1.png" width="400" />
</a>
</div><div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8o3F59qx2j6AB0Gjrn6fc-y5CR9Ke8c34Z6xOLsEFV7x_JInLeBnjJjlxzTRiPqqKTyguf2xJ3b_HpwEJROrSCkSGHYyFuD9zQpRrSOGDNm_iz81Q2R0VSWlCXQgKVRKbO1frOKhsY9Y/s1600/1610706719657116-2.png" style="margin-left: 1em; margin-right: 1em;">
<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8o3F59qx2j6AB0Gjrn6fc-y5CR9Ke8c34Z6xOLsEFV7x_JInLeBnjJjlxzTRiPqqKTyguf2xJ3b_HpwEJROrSCkSGHYyFuD9zQpRrSOGDNm_iz81Q2R0VSWlCXQgKVRKbO1frOKhsY9Y/s1600/1610706719657116-2.png" width="400" />
</a>
</div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: left;">洒落っ気も何も無いですけど、とにかく軽くて、そこそこ良いCPU積んでるんです。</div><br /></div><div>仕様は<a href="https://dynabook.com/direct/pc/catalog/2020-spring/gz83m/spec.html">このページ</a>にあるとおりですが、特に気に入ってる点は、</div><div>6コアCore i7 CPU、16GBメモリ、512GB NVMe SSD、859gという軽さなどです。</div><div><br /></div><div>あと1GNICやHDMIのコネクタが最初からついていたり、USB Type-C PDで充電できたりするのも、とても便利です。</div><div><br /></div><div>OSはもちろん、Debian Linuxにしてるんですけど、今回はWindowsもデュアルブートできるようにしてあります。</div><div><br /></div><div>会計ソフトやNovaのお茶の間留学のソフトなんかが、Windowsじゃないと動かないので、仕方なく残している感じです。</div><div><br /></div><div>デュアルブートも昔からありますが、最近のノートパソコンはBiosがUEFI化されていて、ちょっと昔とは勝手が違っていたりしました。</div><div><br /></div><div>その辺りも、追々ブログにまとめられたらと思います。</div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-51731335497286759382021-01-15T18:38:00.001+09:002021-01-16T12:55:48.901+09:00Debian LinuxのノートパソコンでドトールWiFiにつなぐ<p>ドトール、エクセルシオールカフェには無料のWiFiサービスがある。Linuxで接続するのにちょっと手間取ったので、やり方をメモしておく。</p><p>設定ファイルを作成。</p>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">/etc/network/interfaces.d/doutor
iface dt inet dhcp
wpa-ssid "DOUTOR_FREE_Wi-Fi"
wpa-key-mgmt NONE
</pre></div>
<p>インターフェースをアップするとDHCP経由でアドレスの取得ができる。</p>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">ktaka@dyna:~$ sudo ifup wlan0=dt
Internet Systems Consortium DHCP Client 4.4.1
Copyright 2004-2018 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/
Listening on LPF/wlan0/3c:9c:0f:8d:c8:4a
Sending on LPF/wlan0/3c:9c:0f:8d:c8:4a
Sending on Socket/fallback
DHCPDISCOVER on wlan0 to 255.255.255.255 port 67 interval 8
DHCPDISCOVER on wlan0 to 255.255.255.255 port 67 interval 14
DHCPOFFER of 10.68.161.211 from 10.68.128.2
DHCPREQUEST for 10.68.161.211 on wlan0 to 255.255.255.255 port 67
DHCPACK of 10.68.161.211 from 10.68.128.2
bound to 10.68.161.211 -- renewal in 138 seconds.
</pre></div>
<p>このあとブラウザで任意のページを閲覧しようとするとWEBの認証ページが表示されるので、承諾すればネットワークが使用可能になる。</p>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-88748600719011633802020-11-25T23:14:00.010+09:002020-11-26T15:53:20.087+09:00pingが通らない時pingができなくなるのは、うっかりファイルシステムをコピーした際にcapabilityを落としてしまったことが原因であることもあるらしい。<div>rsyncやtarでファイルシステムごとコピーする際は、--xattrsを忘れずに。<br /><div><br /></div><div>pingが通らない。</div><div><br /><!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">$ ping str
ping: socket: Address family not supported by protocol
$ ping -4 str
ping: socket: Operation not permitted
</pre></div><div><br /></div>
capabilityの確認。何も付いてない。</div><div><br /><!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">$ sudo getcap /bin/ping
</pre></div><div><br /></div>
capabilityの付与と確認。</div><div><br /><!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">$ sudo setcap cap_net_raw=ep /bin/ping
$ sudo getcap /bin/ping
/bin/ping = cap_net_raw+ep
</pre></div><div><br /></div><div>"cap_net_raw+ep"が付与された。</div><div><br /></div>そしてping通るようになった。</div><div> <!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">$ ping str
PING stretch.h.ccmp.jp (192.168.60.3) 56(84) bytes of data.
64 bytes from stretch.h.ccmp.jp (192.168.60.3): icmp_seq=1 ttl=64 time=6.89 ms
64 bytes from stretch.h.ccmp.jp (192.168.60.3): icmp_seq=2 ttl=64 time=3.100 ms
64 bytes from stretch.h.ccmp.jp (192.168.60.3): icmp_seq=3 ttl=64 time=3.53 ms
</pre></div><div><br /></div>以下ちょっと実験する際などに役立つコマンド達 </div><div><div><br /></div><div>capability, xattrの確認コマンド、何もない時</div><div> <!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># getcap /bin/ping
# getfattr -n security.capability /bin/ping
/bin/ping: security.capability: No such attribute
# sudo getfattr -d /bin/ping
# sudo attr -l /bin/ping
</pre></div><div><br /></div>
capability, xattrの確認コマンド、権限がちゃんと付与されている時</div><div><br /><!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># getcap /bin/ping
/bin/ping = cap_net_raw+ep
# attr -l /bin/ping
Attribute "capability" has a 20 byte value for /bin/ping
# getfattr -n security.capability /bin/ping
getfattr: Removing leading '/' from absolute path names
# file: bin/ping
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
</pre></div><div><br /></div>xattrごと削除するコマンド</div><div> <!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># setfattr -x security.capability /bin/ping
</pre></div><div><br /></div>
参考文献</div><div><ul style="text-align: left;"><li><a href="https://man7.org/linux/man-pages/man5/attr.5.html" target="_blank">https://man7.org/linux/man-pages/man5/attr.5.html</a></li><li>tarの場合 --xattrsを付ける <a href="https://www.gnu.org/software/tar/manual/html_node/Extended-File-Attributes.html" target="_blank">https://www.gnu.org/software/tar/manual/html_node/Extended-File-Attributes.html</a></li><li>rsyncの場合も --xattrs, -Xを付ける <a href="https://man7.org/linux/man-pages/man1/rsync.1.html" target="_blank">https://man7.org/linux/man-pages/man1/rsync.1.html</a></li><li><a href="https://wiki.archlinux.org/index.php/capabilities" target="_blank">https://wiki.archlinux.org/index.php/capabilities</a></li></ul></div></div></div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-5515588160922729182020-05-17T18:57:00.013+09:002021-01-23T18:51:11.197+09:00LinuxでのSLAAC IPv6アドレス自動設定<div>Linuxにおいて、SLAACによるIPv6アドレスアサインにはいくつかのモードがあり、以下のカーネルパラメータをセットすることで制御できるようです。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">addr_gen_mode - INTEGER
Defines how link-local and autoconf addresses are generated.
0: generate address based on EUI64 (default)
1: do no generate a link-local address, use EUI64 for addresses generated
from autoconf
2: generate stable privacy addresses, using the secret from
stable_secret (RFC7217)
3: generate stable privacy addresses, using a random secret if unset
</pre></div>
<div><br /></div><div>モード2のRFC7217形式を利用するためには以下のように<span style="font-family: "hiragino kaku gothic pron"; white-space: pre-wrap;">、</span><span style="font-family: "hiragino kaku gothic pron"; white-space: pre-wrap;">stable_secretをあらかじめセットしておく必要があります。</span></div><div><span style="font-family: "hiragino kaku gothic pron"; white-space: pre-wrap;"><br /></span></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">stable_secret - IPv6 address
This IPv6 address will be used as a secret to generate IPv6
addresses for link-local addresses and autoconfigured
ones. All addresses generated after setting this secret will
be stable privacy ones by default. This can be changed via the
addrgenmode ip-link. conf/default/stable_secret is used as the
secret for the namespace, the interface specific ones can
overwrite that. Writes to conf/all/stable_secret are refused.
It is recommended to generate this secret during installation
of a system and keep it stable after that.
By default the stable secret is unset.
</pre></div>
<div><span style="white-space: pre-wrap;"><br /></span></div><div><span style="white-space: pre-wrap;"><a href="https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt">https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt</a></span></div><div><br /></div><h4 style="text-align: left;">モード0:</h4><div><a href="https://tools.ietf.org/html/rfc4291#appendix-A">RFC4291</a>形式のアドレスを自動生成します。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># echo 0 > /proc/sys/net/ipv6/conf/eth0/addr_gen_mode
# ifdown eth0; ifup eth0
# ip -6 add show dev eth0
4: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 state UP qlen 1000
inet6 fd00::40:c23f:d5ff:fe69:4af3/64 scope global dynamic mngtmpaddr
valid_lft 86383sec preferred_lft 14383sec
inet6 fe80::c23f:d5ff:fe69:4af3/64 scope link
valid_lft forever preferred_lft forever
</pre></div>
<div><br /></div><div>IPv6アドレスはRAから得られたPREFIX(fd00::40/64)と、MACアドレス(c0:3f:d5:69:4a:f3)を変換したEUI64アドレス(c23f:d5ff:fe69:4af3)から生成されていることがわかります。</div><div><br /></div><h4 style="text-align: left;">モード1:</h4><div>アドレスを自動生成しません。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># echo 1 > /proc/sys/net/ipv6/conf/eth0/addr_gen_mode
# ifdown eth0; ifup eth0
# ip -6 add show dev eth0
# </pre></div>
<div><br /></div><h4 style="text-align: left;">モード2:</h4><div>Semantically Opaque Interface Identifiers(SOII, <a href="https://tools.ietf.org/html/rfc7217">RFC7217</a>)形式のアドレスを自動生成します。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># echo "::" > /proc/sys/net/ipv6/conf/eth0/stable_secret
# echo 2 > /proc/sys/net/ipv6/conf/eth0/addr_gen_mode
# ifdown eth0; ifup eth0
# ip -6 add show dev eth0
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fd00::40:c81b:6763:31dc:887a/64 scope global dynamic mngtmpaddr stable-privacy
valid_lft 86398sec preferred_lft 14398sec
inet6 fe80::df74:fff4:bdf2:e8ae/64 scope link stable-privacy
valid_lft forever preferred_lft forever
</pre></div>
<div><br /></div><div>IPv6アドレスはRAから得られたPREFIX(fd00::40/64)と、<a href="https://tools.ietf.org/html/rfc7217">RFC7217</a>に定められた方式で計算されるランダム識別子から生成されます。ランダム識別子は次のような関数から計算されsecret_keyに変更がなければ(すなわちstable_secretがセットされていれば)、毎回同じものになります。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">RID = F(Prefix, Net_Iface, Network_ID, DAD_Counter, secret_key)</pre></div><div><br /></div><h4 style="text-align: left;">モード3:</h4><div>Semantically Opaque Interface Identifiers(SOII, RFC7217形式のアドレスを自動生成します。stable_secretがセットされている場合にはモード2と同じアドレスを生成し、されていない場合にはrandom secretを用いアドレスを生成します。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;"># echo 3 > /proc/sys/net/ipv6/conf/eth0/addr_gen_mode
# ifdown eth0; ifup eth0
# ip -6 add show dev eth0
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fd00::40:73be:87b2:8746:6bb3/64 scope global dynamic mngtmpaddr stable-privacy
valid_lft 86395sec preferred_lft 14395sec
inet6 fe80::ddae:cc16:47e0:156d/64 scope link stable-privacy
valid_lft forever preferred_lft forever
</pre></div>
<div><br /></div><div>モード2との違いは、stable_secretがセットされていない場合に、毎回異なるランダム識別子からIPv6アドレスが生成されるということです。</div><div><br /></div><div>addr_gen_modeのパラメータは以下のコマンドでもセットできるようです。</div><div><br /></div>
<!--HTML generated using hilite.me--><div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;"><pre style="line-height: 125%; margin: 0px;">ip link set dev eth0 addrgenmode { eui64 | none | stable_secret | random }
</pre></div><div><br /></div><h4 style="text-align: left;">
参考文献</h4><div>「クライアントOSのIPv6実装検証から見たネットワーク運用における課題の考察」</div><div><a href="https://www.ipsj.or.jp/dp/contents/publication/36/S0904-R1701.html">https://www.ipsj.or.jp/dp/contents/publication/36/S0904-R1701.html</a></div><div>「TIPS: 拡張EUI-64を使わないIPv6アドレス生成」</div><div><a href="http://www.drvlabo.jp/wp/archives/1713">http://www.drvlabo.jp/wp/archives/1713</a><br /><div><br /></div></div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-36838731557055004612020-05-05T20:21:00.005+09:002020-06-18T16:26:21.656+09:00LinuxルーターでのDS-Lite設定昨日に引き続き、自宅のLinuxルーター上でDS-Liteの設定を行いました。<br />
<br />
BB.exciteコネクト(IPoE接続プラン)ではIPv6回線のトンネルを通して、インターネットにIPv4接続することが可能です。<br />
実際のサービスはtransixのDS-Liteを使うことになるのですが、この回線を使うことで混み合っているNGNの網終端を通らずに済むことになるので、快適なネットライフが待っているらしいのです。<br />
<br />ところが我が家ではすでにPPPoEでのIPv4の固定IP契約があり、その固定IPをVPN、DNSサーバなどいろいろなサービスに使っていて、別のIPアドレスに変わってしまって困ります。またDS-Liteの場合にはグローバルIPアドレスを複数のユーザーで共有して使うので、なおさら外部からのアクセスなどには使うことができません。<div>そういった理由で、IPv4に関しては既存のPPPoEのままにするつもりでいました。</div><div><br />しかしながらWebページの閲覧やYutubeコンテンツの視聴に使われるHTTPやHTTPSに限れば固定IPは不要です。また、Linuxルーターの場合には、宛先のIPやプロトコル、ポート番号に応じてきめ細かく接続先の経路を選ぶことができます。そこで、せっかくなので今回、HTTPとHTTPSに限って、DS-Liteの経路を利用する設定を試してみました。</div><div>
<br />
IPv4 over IPv6のトンネルセットアップ<br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">ip -6 tunnel add dslite mode ip4ip6 \
remote 2404:8e00::feed:100 local 2409:x:x:x::1 dev eth0.10
ip link set dev dslite up</pre>
</div>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;"><br /></span>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;">宛先が</span><span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;">192.168.0.0/16以外(宛先がWAN向き)で、宛先</span><span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; letter-spacing: 0.1px; white-space: pre-wrap;">ポート</span><span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; letter-spacing: 0.1px; white-space: pre-wrap;">80、443のパケットにマークをつける。</span><br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">iptables -A PREROUTING -i eth0 -t mangle \
! -d 192.168.0.0/16 -p tcp --dport 443 -j MARK --set-mark 1
iptables -A PREROUTING -i eth0 -t mangle \
! -d 192.168.0.0/16 -p tcp --dport 80 -j MARK --set-mark 1
</pre>
</div>
<span style="color: #202124; font-family: roboto, arial, sans-serif;"><span style="background-color: white; letter-spacing: 0.1px; white-space: pre-wrap;"><br /></span></span>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;"></span><br />
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; letter-spacing: 0.1px; white-space: pre-wrap;">マークがついているパケットだけDS-Liteのトンネルを通るようにする。</span><br />
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; letter-spacing: 0.1px; white-space: pre-wrap;"><br /></span>
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">ip route add default dev dslite table 100
ip rule add from all fwmark 1 table 100 prio 100</pre>
</div>
<div>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;"></span><br />
<div>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;"><span style="color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;">これで自宅LAN内の任意のマシンからDS-Lite回線をHTTP、HTTPS接続できるようになりました。</span></span></div><div><font color="#202124" face=""><span style="letter-spacing: 0.1px; white-space: pre-wrap;"><br /></span></font></div><div><font color="#202124" face=""><span style="letter-spacing: 0.1px; white-space: pre-wrap;">経路が変わっているかどうかは、WEB上にあるアクセス元IPを判別するページ(例えば<a href="https://www.cman.jp/network/support/go_access.cgi" target="_blank">ここ</a></span></font><font color="#202124" face=""><span style="letter-spacing: 0.1px; white-space: pre-wrap;">)などで確認可能で、</span></font><span style="color: #202124; font-family: roboto, arial, sans-serif; letter-spacing: 0.1px; white-space: pre-wrap;">ちゃんと切り替わっていることが確認できました。</span></div><div><font color="#202124" face=""><span style="letter-spacing: 0.1px; white-space: pre-wrap;"><br /></span></font>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;"><span style="color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;">いままで回線の遅さに悩んでいましたが、これで解決されるのか楽しみです。</span></span></div>
<span style="background-color: white; color: #202124; font-family: roboto, arial, sans-serif; font-size: 16px; letter-spacing: 0.1px; white-space: pre-wrap;">
</span></div>
<br /></div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-19101462441973182272020-05-05T00:48:00.015+09:002021-06-11T15:06:06.593+09:00LinuxルーターでのIPv6 IPoE設定こんにちは、コロナの影響でステイホームでリモートワークを励行しています。多くの人たちが業務のために自宅からインターネットにアクセスしたり、外に遊びに行けず自宅でネットコンテンツを長時間視聴するためか、日時を問わずNTTフレッツ回線のネットワークが非常に遅くなっているような気がします。<br />
<br />
フレッツ回線の混雑の情報をネットで調べてみると、もともと回線設備の容量が少なめで、主にIPv4のPPPoE接続で利用しているNTTのNGNの網終端装置で回線が混み合っているのではないかということです。<br />
(<a href="https://eng-blog.iij.ad.jp/archives/5536">https://eng-blog.iij.ad.jp/archives/5536</a> <a href="http://enog.jp/wp-content/uploads/2018/08/20180720-ENOG51-Kashiwazaki.pdf">http://enog.jp/wp-content/uploads/2018/08/20180720-ENOG51-Kashiwazaki.pdf</a>)<br />
<br />
これを回避するには比較的空いているIPv6の接続口を利用するのが良いとのことでした。NTTのNGNネットワークとインターネットの境界の設備は、NTTの方針に依存せず増強できるとのことです。<br />
<br />
そこでIPv6 IPoE + DS-Lite なプロバイダと契約し、もともと自宅で利用していたLinuxルーターをIPv6に対応させました。<br />
<br />
今回契約したのはBB.exciteコネクト(IPoE接続プラン)というもので、既存のフレッツ回線に月々700円(税抜)追加でIPv6のインターネット接続を利用できるようになります。<br />
<a href="https://bb.excite.co.jp/norikae/ipoe/">https://bb.excite.co.jp/norikae/ipoe/</a><br />
<br />ちなみに、本題から外れますが、我が家のルーターは手のひらサイズのIntelのNUCです。NICが一個しかついていないため、eth0からTag VLANを利用してeth0.10のインターフェースを仮想的に作りWAN側に用いています。eth0とeth0.10を後述のコマンド例で使いますが、前者が内側、後者が外側ということです。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcAPmp-KJnKKRfcr3BykhDsr8dH6qgd2OClIMRoO4b6HPYOXfnBwK_BeW3nLYGLaWzI-vpXJowMQXoRyXCx0l3CcqhigfKq4SxRju3ki7Poa7MR1_uIiAn7UPG09pBtYGzN-TID3WIFuA/s1600/2020-05-04+%25284%2529.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="960" data-original-width="1280" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcAPmp-KJnKKRfcr3BykhDsr8dH6qgd2OClIMRoO4b6HPYOXfnBwK_BeW3nLYGLaWzI-vpXJowMQXoRyXCx0l3CcqhigfKq4SxRju3ki7Poa7MR1_uIiAn7UPG09pBtYGzN-TID3WIFuA/s320/2020-05-04+%25284%2529.jpg" width="320" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrlWeb5x8YUX8YwiHXSo19ItgLg_e-v365fs8_heHenmuEFiIWjO6n7tDfXMAg-HcCfjTYLeD55lnZk2q7Jo00yh3HIH-jBk3iYh7JpAPfmLzKjzzXzZnT6TFxl0OTZyHAdN_XJVI_Nzc/s1600/2020-05-04+%25285%2529.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="960" data-original-width="1280" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrlWeb5x8YUX8YwiHXSo19ItgLg_e-v365fs8_heHenmuEFiIWjO6n7tDfXMAg-HcCfjTYLeD55lnZk2q7Jo00yh3HIH-jBk3iYh7JpAPfmLzKjzzXzZnT6TFxl0OTZyHAdN_XJVI_Nzc/s320/2020-05-04+%25285%2529.jpg" width="320" /></a></div>
<br />ちなみに、ルーターのOSはDebian Busterをtarで固めたものをUSBメモリに入れ、起動時にtmpfsに展開して、ディスクレスで運用しています。こうすることで、万が一NUCが壊れたとしても適当なパソコンをルーターとしてブートすることが可能です。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoSaGbwFx7vufzf28Kp0bWKTYLR4uNeiBd-V9e8G5zxJwBesf8b4fzdu2tIefMxVz8yHSluqf3CAO_PHwveGPcb13s_hMGnfyRsqAb6TZNirfT4NYMbxw123HxmuQYpsKMSlQnPif68Vc/s1600/2020-05-04+%25283%2529.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="960" data-original-width="1280" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoSaGbwFx7vufzf28Kp0bWKTYLR4uNeiBd-V9e8G5zxJwBesf8b4fzdu2tIefMxVz8yHSluqf3CAO_PHwveGPcb13s_hMGnfyRsqAb6TZNirfT4NYMbxw123HxmuQYpsKMSlQnPif68Vc/s320/2020-05-04+%25283%2529.jpg" width="320" /></a></div>
<br />
<br />
<h3>
WANインターフェースの設定</h3>
<br />
IPv6の世界では、Stateless Address Auto-Configuration(SLAAC)という仕組みがあり、基本的に何もしなくてもIPv6アドレスがアサインされます。<br />
<br />
Linuxの場合、IPv6関連のコンフィグオプションを有効にしたカーネルがあり、かつicmpv6のパケットを送受信できる環境であれば、いつの間にか勝手にIPv6アドレスが付与されます。<br />
<br />
今回契約したのBB.exciteコネクト(IPoE接続プラン)は、transixというVirtual Network Enabler(VNE)が実際の通信サービスを提供しており、transixから払い出された/64のプリフィックスがアドバタイズ(Router Advertisement (RA))され、それを受信したLinuxカーネルが、自動的にネットワーク設定を行います。<br />
<br />
RAの内容はこのような感じになっています。
<!--HTML generated using hilite.me--><br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;"># /bin/rdisc6 -1 -r 3 eth0.10
Soliciting ff02::2 (ff02::2) on eth0.10...
Hop limit : 64 ( 0x40)
Stateful address conf. : No
Stateful other conf. : Yes
Mobile home agent : No
Router preference : medium
Neighbor discovery proxy : No
Router lifetime : 1800 (0x00000708) seconds
Reachable time : 300000 (0x000493e0) milliseconds
Retransmit time : 10000 (0x00002710) milliseconds
Source link-layer address: 00:yy:yy:yy:53:C3
MTU : 1500 bytes (valid)
Prefix : 2409:x:x:x::/64
On-link : Yes
Autonomous address conf.: Yes
Valid time : 2592000 (0x00278d00) seconds
Pref. time : 604800 (0x00093a80) seconds
from fe80::y:y:y:53c3
</pre>
</div>
(参考: <a href="https://hirose31.hatenablog.jp/entry/20060418/1145354566">https://hirose31.hatenablog.jp/entry/20060418/1145354566</a> )
<br />
<br />
上記のRAに基づいて自動的に設定されたIPアドレスの様子。<br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">eth0.10@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether c0:3f:d5:69:6f:b0 brd ff:ff:ff:ff:ff:ff
inet6 2409:x:x:x:y:y:y:6fb0/64 scope global dynamic mngtmpaddr
</pre>
</div>
<br />
同時に登録されるルーティング情報。<br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">2409:x:x:x::/64 dev eth0.10 proto kernel metric 256 pref medium
default via fe80::y:y:y:53c3 dev eth0.10 proto ra metric 1024 expires 1784sec hoplimit 64 pref medium
</pre>
</div>
<br />
SLAACによりアサインされるアドレスは、transixから払い出されたプリフィックス (2409:x:x:x::/64)とNICのMACアドレスを組み合わせたものです。<br />
このようなIPv6アドレスを用いるとMACアドレスが一意であるため、これを用いてユーザーの行動が追跡される可能性があるなどプライバシーの上の問題があります。<br />
この問題に対処するためにPrivacy Extensionという仕様が公開されており、これを用いると払い出されたプリフィックスとランダムな数字を組み合わせたIPv6アドレスを利用することができます。<br />
<br />
つまりPrivacy Extensionを有効にするとMACアドレス由来でないアドレスもインターフェースにアサインすることができるのです。Linuxの場合はuse_tempaddrというパラメータを利用します。<br />
<a href="https://www.tldp.org/HOWTO/Linux+IPv6-HOWTO/ch06s05.html">https://www.tldp.org/HOWTO/Linux+IPv6-HOWTO/ch06s05.html</a><br />
<a href="https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt">https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt</a><br />
<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">use_tempaddr - INTEGER
Preference for Privacy Extensions (RFC3041).
<= 0 : disable Privacy Extensions
== 1 : enable Privacy Extensions, but prefer public
addresses over temporary addresses.
> 1 : enable Privacy Extensions and prefer temporary
addresses over public addresses.
Default: 0 (for most devices)
-1 (for point-to-point devices and loopback devices)
</pre>
</div>
<br />
実際にパラメータを設定してみると、<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">echo 2 > /proc/sys/net/ipv6/conf/eth0.10/use_tempaddr
</pre>
</div>
<br />
Temporaryなアドレスもアサインされます。<br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;"> inet6 2409:x:x:x:y:y:y:d384/64 scope global temporary dynamic
valid_lft 569580sec preferred_lft 50999sec</pre>
</div>
<div><br /></div><div>(追記:最近ではSemantically Opaque Interface Identifiersと呼ばれる、よりプライバシーに考慮したアドレスアサイン方式があるようなのでそれにも<a href="/2020/05/linuxslaac-ipv6.html">対応しました</a>。)</div><br />
<h3>
LANインターフェースの設定</h3>
<div>
<br />
次にLAN側のネットワークをどうするか考えます。まず思いつくのは以下の3通りの方法です。<br />
<ol>
<li>WANインターフェースとLANインターフェースをブリッジ接続し、WANから流れてきたRAをLANにそのまま流す。</li>
<li>WAN側で受け取ったグローバルなプリフィックスを、radvdというプログラムでLANに再広報する。</li>
<li>WAN側とは別のプライベートなIPv6アドレス(ユニークローカルアドレス)のプリフィックスをradvdでLANに広報する。</li>
</ol>
</div>
<div>
<div>
ある程度の試行錯誤の末、上記のうち3の方法を採用することにしました。</div><div><br /></div>
</div>
<div>
まず1の構成では、IPv6に関してはL3レベルでネットワークが分離されていないため、やはりセキュリティ設定に不安があります。まあ要するにLAN内に自分の知らないicmpv6のパケットが入ってくること自体が気持ち悪くて受け入れられないということです。</div><div><br /></div>
<div>
2の構成に関しては、以下に示すような技術課題があり、とりあえず設定はできるのですが、上手いルーティング設定とSLAAC自動設定が両立できません。 </div>
<ul>
<li>WANとLANで同じプリフィックスを利用すると、ゲートウェイ上でのルーティング設定がめんどう。</li>
<li>/64を/65に分割してつかうとSLAACが機能しない。</li>
</ul>
WANで受け取ったプリフィックスを利用しLAN側のeth0にアドレスをアサインすると、ルーティングテーブルが以下のようになります。<br />
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">2409:x:x:x::/64 dev eth0.10 proto kernel metric 256 expires 2591991sec pref medium
2409:x:x:x::/64 dev eth0 proto kernel metric 256 pref medium
</pre>
</div>
<br />
この場合の問題点は、2409:x:x:x::/64宛のパケットに対するルーティング設定が2行あり、eth0側にパケットを流したくても先にあるeth0.10側にパケットが流れてしまい、通信がうまくいきません。<br />
<br />
2409:x:x:x::/64向けのパケットはeth0側に流したいので、1行目を削除すれば良いのですが、この1行目の設定は、WAN側のSLAACの自動設定により自動追加されるものなので、しばらくするとまた追加されてしまいます。<br />
<br />
この問題を回避するために2409:x:x:x::/64で許されるアドレスレンジの内半分の2409:x:x:x:8000::/65を、LAN内プリフィックスとして利用することを試みました。ルーティングルールでは長いプリフィックスの方が優先されるので、2409:x:x:x:8000::/65宛のパケットを必ずeth0に流すことができるというもくろみです。<br />
<br />この方法でルーティング自体はうまくいくのですが、SLAACによる自動アドレス設定はプリフィックスが64であることが前提であるため、各マシンに手動でIPv6アドレスを設定する必要が出てきます。<div><br /></div><div>つまり、IPoEで配布されるグローバルプリフィックスが/64である場合、ルーティングをきれいに動かすためにLANのプリフィックスをそれより長くすると、SLAACによるLAN内の自動アドレス設定ができなくなってしまいます。<br /><div><br /></div><div>(ひかり電話契約がある場合には/56のプレフィックスが配布されるらしいが、我が家はひかり電話では無いので/64で。。。みんな/56とか、せめて/63で配布してくれれば良いのに。)<br />
<br />結局、このような理由から、「2. WAN側で受け取ったグローバルなプリフィックスを、radvdというプログラムでLANに再広報する。」という方法は断念しました。<br />
<br />
<h3>
LAN側はプライベートなアドレスを利用</h3>
<br />
仕方が無いので、LAN内ではユニークローカルアドレスfd00::/8を利用する第3の方法を用いることにしました。具体的にはradvdでfd00:0:0:x::/64なプリフィックスを配布します。そうするとLAN内の様々なクライアントがSLAACにより自動的にアドレスを生成し、IPv6により通信できるようになります。<br />
<br />
/etc/radvd.confの例<br />
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">interface eth0
{
AdvSendAdvert on;
MinRtrAdvInterval 30;
MaxRtrAdvInterval 100;
prefix fd00:0:0:x::/64
{
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr off;
};
RDNSS fd00:0:0:x::1
{
};
};
</pre>
</div>
<br />
この構成だとLAN内はグローバルIPではなく、外に出る際にMASQUERADEすることが必要になります。NATが必要無いというIPv6のメリットの一つを損なってしまいますが仕方が無いです。<br />
考えようによっては、IPv4で慣れ親しんだ構成でもあるので、管理は難しく無いというメリットもあります。<br />
外部からのL3フィルタリング設定も、慣れ親しんだiptableと同様に行うことができます。<br />
<br />
ip6tables設定。 <br />
/etc/ip6t.save (使用例 /sbin/ip6tables-restore < /etc/ip6t.save)<br />
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A POSTROUTING -o eth0.10 -j MASQUERADE # <- マスカレード設定
COMMIT
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -s fd00:0:0:x::/64 -i eth0 -j ACCEPT
-A INPUT -p udp --dport 53 -j ACCEPT
-A INPUT -p tcp --dport 53 -j ACCEPT
-A INPUT -p ipv6-icmp -j ACCEPT
-A FORWARD -d fd00:0:0:x::/64 -j ACCEPT
-A FORWARD -s fd00:0:0:x::/64 -i eth0 -o eth0.10 -j ACCEPT
-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
COMMIT
</pre>
</div>
<br />
<h3>
外部からのアクセス</h3>
<br />
SLAACにより設定されるアドレスは、NICのMACアドレスから生成されるため決して覚えやすいものではありません。"プリフィックス::1"という分かり易いアドレスをマニュアルアサインすることで、外部からのアクセスが僅かながら容易になります。<br />
<br />
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">/sbin/ip add add 2409:x:x:x::1/64 dev eth0.10
</pre>
</div>
<br />
ただしtransixから払い出されるプリフィックスは固定であるという約束はありません。プリフックス変わった場合にはそれを検知してマニュアルアサインしたアドレスも付け直し、そしてDNSなどを更新する必要があるでしょう(これについてはいまのところ未実装です) 。<br />
<br />
固定のプリフィックスを払い出してくれるISPがあったら乗り換えたいです。<br />
<br />
<h3>
</h3>
<h3>
</h3>
<h3>
IPv4をどうするか</h3>
<br />
IPv4に関してはPPPoE接続の固定IPの契約が元々あるので、そのまま利用することにしました。<br />
<br />
今回契約した、DS-Liteのサービスでは、IPv4 NATとIPIPトンネリングを利用したIPv4 over IPv6接続が利用できます。PPPoE接続をやめてこちらの経路を利用することでIPv4回線も空いている方を利用できることになりますが、グローバルなIPv4アドレスを他のユーザと共用する必要があるため、いまのところそれほど魅力を感じていません。<br /><br />
transixも固定IPのサービスがあるのでそれが利用可能かつ安価なISPがあれば考えたいです。(<a href="https://www.mfeed.ad.jp/transix/staticip/">https://www.mfeed.ad.jp/transix/staticip/</a>)<br />
<br />(追記:後日、ポリシールーティングを使ってHTTP、HTTPSの通信のみDS-Liteを使うようにしました。→<a href="/2020/05/linuxds-lite.html">こちら</a>)<br /><br />
別のVNEである、JPNEからも同様な、v6オプション固定IPサービスがあるようなので、こちらも安価なISPがあれば考えたいです。(<a href="https://www.jpne.co.jp/service/v6plus-static/">https://www.jpne.co.jp/service/v6plus-static/</a>)<br />
<br />
<h3>
その他の注意点</h3>
<br />
Linuxボックスでipv6のフォワーディングを有効にするとルーティング情報が消えてしまい、これに悩まされました。<br />
forwarding=1でもRouter Advertisement(RA)を受信できるようにするために、以下が必要です。<br />
<br />
<!--HTML generated using hilite.me--><br />
<div style="background: rgb(248, 248, 248); border: none; overflow: auto; width: auto;">
<pre style="line-height: 125%; margin: 0px;">echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
echo 2 > /proc/sys/net/ipv6/conf/eth0.10/accept_ra</pre>
</div>
( <a href="http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/">http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/</a> )<br />
<br />
<h3>
まとめ</h3>
<br />
IPv6 IPoE + DS-Lite なプロバイダと契約し、LinuxルーターのIPv6設定を行いました。WAN側アドレスはSLAACにより自動的に設定されるものを用いました。LAN側アドレスはユニークローカルアドレスを利用し、MASQUERADEとフィルタリングの設定を行いました。<br />
今後の課題としては、(1)64より短いプリフィックスが得られるなら、LAN内もグローバルアドレスを利用したい、(2)LinuxのルーティングがSLAACと両立できるようになったら、LAN内もグローバルアドレスを利用したい、(3)固定プリフィックスが利用できるISPがあれば乗り換えたい、(4)固定グローバルIPのIPv4 over IPv6が利用可能かつ安価なISPがあれば乗り換えたい、などがあります。<br />
<br /></div></div>ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com2tag:blogger.com,1999:blog-4279971486311743746.post-18376250727938601602015-12-22T23:54:00.002+09:002019-03-04T18:11:52.428+09:00Keepalivedで作るMySQLフェイルオーバーシステム<h2>
<span style="color: #333333;"><span style="background-color: white; line-height: 24px;">1. はじめに</span></span></h2>
<span style="background-color: white; color: #333333; line-height: 24px;">この記事は</span><a href="http://qiita.com/advent-calendar/2015/mysql-casual" style="background-color: white; color: #548fcb; line-height: 24px;" target="_blank">MySQL Casual Advent Calendar 2015</a><span style="background-color: white; color: #333333; line-height: 24px;">の22日目のエントリです。</span><br />
<div>
先日、<a href="http://mysql-casual.connpass.com/event/22334/">MySQL Casual Talks</a>という勉強会で登壇してきました。その時の内容をまとめておきたいと思います。</div>
<div>
<br /></div>
<div>
MySQLデータベースサーバに障害が起きた時、サービスを続けるには幾つかの方法があります。障害発生時にSlaveサーバーを手作業でMasterに昇格させる方法、<a href="http://dev.mysql.com/downloads/utilities/">MySQL Utilities</a>に含まれるmysqlfailoverというユーティリティーを利用する方法などです。<br />
<br />
今回、Keepalivedというソフトウェアと、MySQLの双方向レプリケーションを使って、ほぼ無停止でフェイルオーバーする構成を試してみたので、それについてまとめておきたいと思います。</div>
<div>
<br /></div>
<div>
<h2>
2. システム構成</h2>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVgvVXK57xoXPZfjwbqlmnUljvngqNRM2RM_KSX8o8LsZcwA4TrNgSDj-CLx7ZUmrutNnySRGdZhtPYPP7PsE0IeRKmUi2ZTv0Y3XwGVkLtogP8uBYTm4hdlKTv4LLJpQM27XVDd00mDo/s1600/Fig1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVgvVXK57xoXPZfjwbqlmnUljvngqNRM2RM_KSX8o8LsZcwA4TrNgSDj-CLx7ZUmrutNnySRGdZhtPYPP7PsE0IeRKmUi2ZTv0Y3XwGVkLtogP8uBYTm4hdlKTv4LLJpQM27XVDd00mDo/s1600/Fig1.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
db1、db2という二つのサーバで、それぞれmysqldとkeepalivedを動かします。mysqldはお互いがMasterとなる双方向レプリケーションを行います。keepalivedはそれぞれのサーバで<a href="https://github.com/ktaka-ccmp/mysql-casual-20151220/blob/mysql56/files/etc/keepalived/vrrp/mysqlchk.sh">mysqldのヘルスチェック</a>を行い、どちらかがVRRPのMasterもう一方がBackupの状態となり、MasterにVirtual IP(VIP)を付与します。上の図ではdb1が現在のVRRP Masterということになります。双方向レプリケーションを行う場合に同時に両方のサーバへの書き込みを許可すると、データの不整合や、レプリケーションの停止を引き起こしてしまうことが知られています。今回VRRPスレーブに誤って書き込みが行われないようにスレーブにreadonly=1のフラグを立てておきます。</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLl0J0OioNn-1l6pOZumEtRDY81qtsWj9k_ACcnMMZhFieXweWu9Tx-tlXo9qCnMttwCJ-LK7uImTM4q1RmILDTbEQmIP3L-e_0_ic0DHHAZdQ7yq-KUCUS92njiFekWBcO_A8mVk06CY/s1600/Fig2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLl0J0OioNn-1l6pOZumEtRDY81qtsWj9k_ACcnMMZhFieXweWu9Tx-tlXo9qCnMttwCJ-LK7uImTM4q1RmILDTbEQmIP3L-e_0_ic0DHHAZdQ7yq-KUCUS92njiFekWBcO_A8mVk06CY/s640/Fig2.png" width="640" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div>
もしdb1のmysqldが停止ししkeepalivedのヘルスチェックに失敗した場合、db1のkeepalivedはFAULTステートに移行します。一方db2のkeepalivedはVRRP Masterステートに移行しVIPが付与され、クライアントからのアクセスはdb2に向かうようになります。db2のkeepalivedがMasterに昇格する際には、<a href="https://github.com/ktaka-ccmp/mysql-casual-20151220/blob/mysql56/files/etc/keepalived/vrrp/master.sh#L15">スクリプトによりreadonly=0となり書き込みが可能になるようにしています</a>。</div>
<div>
<br />
この構成では、Keepalivedを使うことで迅速かつ安定的にフェイルオーバーが実行できること、あらかじめ双方向レプリケーションが行われているので障害発生時にCHANGE MASTER をする必要がなくシンプルであることがメリットだと考えられます。<br />
<div>
<br /></div>
<br />
<h2>
3. VRRPってなに?</h2>
</div>
<div>
<br /></div>
<div>
VRRP(Virtual Router Redundancy Protocol)は、もともと2台のルーターの冗長化のために作られたプロトコルで、RFCで規定されています。。下の図は、今回利用したKeepalived v1.2.13 が準拠している<a href="https://tools.ietf.org/rfc/rfc3768.txt">VRRP version 2</a>の動きを説明するものです。</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBILIPJUm7tiRBTy1l4sWBmuAY9Lqpgw3bF50VsFURf0k2MeubvTawZhIEZPJwBW3MWHp8b4UNyJFDEPvl2eAVeDW2tK7DOTb_HgJxLkaSSYPHM0mxaxbopoYn2w9HYFj6mCs2SAaYsVA/s1600/Fig3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBILIPJUm7tiRBTy1l4sWBmuAY9Lqpgw3bF50VsFURf0k2MeubvTawZhIEZPJwBW3MWHp8b4UNyJFDEPvl2eAVeDW2tK7DOTb_HgJxLkaSSYPHM0mxaxbopoYn2w9HYFj6mCs2SAaYsVA/s640/Fig3.png" width="640" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div>
VRRP v2では、プライオリティに基づいてMasterとBackupサーバーが決定され、MasterのみがVRRPアドバタイズパケットを予め定められた間隔、例えば1秒間隔で送信します。Masterからのアドバタイズパケットが一定期間途切れると、BackupサーバがMasterサーバに状態遷移します。</div>
<div>
<br /></div>
<div>
<h4>
ヘルスチェック</h4>
</div>
<div>
KeepalivedのVRRPデーモンには、RFCで規定されたVRRPプロコルの機能に加え、おまけとしてヘルスチェック機能があります。これによりスクリプトでmysqldの死活を監視し、監視が失敗するとKeepalivedのステータスをFAULTに遷移させることができます。この場合プライオリティ=0のVRRPアドバタイズパケットが送信され、直ちにBackupサーバがMasterに昇格します。<br />
<br />
以下の設定は、<a href="https://github.com/ktaka-ccmp/mysql-casual-20151220/blob/mysql56/files/etc/keepalived/vrrp/vrrp.conf">keepalivedの設定ファイル</a>の一部ですが1秒に一回ヘルスチェックスクリプト"/etc/keepalived/vrrp/mysqlchk.sh"を実行し、二回失敗したらFAULTステートにし、2回成功するとBACKUPかMASTERに戻しています。<br />
<br /></div>
<div>
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> vrrp_script mysqlchk {
script "/etc/keepalived/vrrp/mysqlchk.sh"
interval 1 ← 1秒ごとにチェック
fall 2 ← 2回失敗したらFAULT
rise 2 ← 2回成功でBACKUP or MASTER
}
</code></pre>
<br />
ヘルスチェックファイル"mysqlchk.sh"の内容は、今回は次のようになっていて、ソケット経由で、"show variables like 'server_id';"に応答できるかどうかを確認しています。<br />
<br /></div>
<div>
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> mysql -S $SOCK --connect-timeout=$TIMEOUT -e "show variables like 'server_id';"
</code></pre>
<br />
<h4>
VRRPのフェイルオーバー時間</h4>
障害時にVRRPのフェイルオーバーにかかる時間は、大まかに以下のふた通りに分けることができます。<br />
<br />
<ul>
<li>mysqldのみ死んだ場合</li>
<ul>
<li>keepalivedがFAULTステートになりpriority=0のVRRP Advertパケットを送信します。</li>
<li>これを受信したBACKUPステートにいるkeepalivedは直ちにMASTERに昇格します。</li>
<li>フェイルオーバーにかかる時間は、FAULTステート遷移に必要な2秒程度です。</li>
</ul>
</ul>
<div>
<br /></div>
<ul>
<li>サーバごと死んだ場合</li>
<ul>
<li>元のサーバからのVRRPパケットが途絶え、BACKUPステートにいるkeepalivedがMASTERステートに昇格します。</li>
<li>BACKUPステートにいるkeepalivedが状態遷移を開始する時間はVRRPプロトコルで決まっており、(3 * Advertisement_Interval) + ( (256 - Priority) / 256 )になります。</li>
<li>VRRPアドバタイズパケットの間隔、すなわちAdvertisement_Intervalが1秒の場合は4秒程度でフェイルオーバーが完了します。</li>
</ul>
</ul>
以上により、この仕組みを利用する場合、2秒または4秒程度でフェイルオーバーが完了することになり、かなり速いと言って良いのではないでしょうか。<br />
<br /></div>
<div>
<h2>
4. 双方向レプリケーションの注意点</h2>
<br />
今回のシステム構成では、db1とdb2の間で双方向のレプリケーション構成にしています。この構成には落とし穴があるので、まず簡単にレプリケーションの仕組みをおさらいしておきたいと思います。<br />
<br />
次の図で、db1でコミットされたトランザクションはbinlogに記録されます。binlogに記録されたトランザクションは、ネットワークを介してdb2に送られIOスレッドによりrelaylogに書き込まれ、SQLスレッドによりテーブルスペースに順次コミッされていきます。<br />
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIkc-k6nMbEhTcZNQZEuw-j-eZ_4ST3qYbFahEqvMQdT49_-XFPJ9X_U-IpGIhBKlOyj2ppBYfKI4UNGKHXz7WERAqiJyZfbx3LdIM0suSZRcuac4PaCPy7Onh22d7YDDcMEvQzT81v3Y/s1600/Fig4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"></a><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWdasRPYsn7I1WjYeuFJ6RIKnE5U2-E2joHNPDysaxhCjRTTyDBhHbcV5yRNwfy_7PwER8Ap04VePBCFgRwHFZdEQNydHKOuwhYy3uHeyKexGgBZM6C1QjoMkBnXv3_ajX1qqokdQ6ai0/s1600/Fig11.png" imageanchor="1"></a><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBzIW0oh8JSdWAfHV2a2HFhSDkf3HKRWSyOFK2dkB-OVNbzLKFHjAg1mtSYGAa3BaDekMGY8M-_hJZ57dyzYbUb_yGJ2nT3D7CpEgu_xHqOb2eFgTjTGBLzil7yGOi7Ahr0OlZLF-jypY/s1600/Fig11.png" imageanchor="1"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBzIW0oh8JSdWAfHV2a2HFhSDkf3HKRWSyOFK2dkB-OVNbzLKFHjAg1mtSYGAa3BaDekMGY8M-_hJZ57dyzYbUb_yGJ2nT3D7CpEgu_xHqOb2eFgTjTGBLzil7yGOi7Ahr0OlZLF-jypY/s640/Fig11.png" width="640" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<span id="goog_2034380117"></span><span id="goog_2034380118"></span>したがって、db1で完了したコミットはすぐにSlaveのテーブルスペースに書き込まれるわけではなく、有限時間の遅延が発生します。特にレプリケーションのSQLスレッドは、mysqld-5.6までは1スキーマに対して1スレッドでしか実行できないので、SQLスレッドによるテーブルスペースへのコミットが追いつかずrelaylogにデータが溜まっていく場合が、時々見られます。<br />
<br />
レプリケーションが遅延している時に、VRRPのフェイルオーバーが起こると以下の図のような状態になります。relaylogに溜まっているデータはSQLスレッドにより順次コミットされていきますが、同時にクライアントからの書き込みが行われると、データに不整合が起こりレプリケーションが停止する場合があります。<br />
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZUtMebWAzyXRO15xuHfnTpyH3aIpSTsng-k6BDcm1XYK8rikPNGhFWrjXS76N15i00Y9BSo2DUC20LcK6FHNPnAttqse6RofWTkqNDHI_uGDc-nmbrpVHzyRf5ohoYHu5sG7UxdhuCXQ/s1600/Fig6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"></a><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi591drEvdVy0XU0cgChVgFzhiyySoV0jFoP94iDGJh_EwCiKQF7xWQ6GPfPYfKDM4AjPy4E9f3G1kwYlYSVVuV2gJn31JtliLdqGLlx8tmXk_pG7-xOpSeh6KZjQoqw6Hr6eFs8CnZRf8/s1600/Fig12.png" imageanchor="1"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi591drEvdVy0XU0cgChVgFzhiyySoV0jFoP94iDGJh_EwCiKQF7xWQ6GPfPYfKDM4AjPy4E9f3G1kwYlYSVVuV2gJn31JtliLdqGLlx8tmXk_pG7-xOpSeh6KZjQoqw6Hr6eFs8CnZRf8/s640/Fig12.png" width="640" /></a></div>
<br />
今回の構成ではこれを防ぐために、フェイルオーバー時にVIPがdb2に移った時、すぐに書き込み可能な状態にするのではなく、レプリケーションの遅延がなくなるのを待っててreadonly=0をセットするようにしています。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> #!/bin/bash
SOCK=/var/run/mysqld/mysqld.sock
while true ; do
mysql -S $SOCK -e "show slave status\G;"|egrep "Seconds_Behind_Master: 0|Seconds_Behind_Master: NULL"
if [ "$?" = "0" ] ; then
break
else
echo Waiting until sql thread finish.
fi
sleep 1
done
mysql -S $SOCK -e "set @@global.read_only=0;"
</code></pre>
<br /></div>
<div>
<h2>
5. まとめ</h2>
</div>
<div>
<br />
今回、keepalivedのVRRP機能と、MySQLの双方向レプリケーションを組み合わせた、MySQLサーバの冗長化について試してみました。<br />
<br />
良いところ</div>
<div>
<div>
<ul>
<li>およそ4秒以内、ほぼ瞬時にフェイルオーバーする。</li>
<li>フェイルオーバー時にレプリケーション関連のオペレーションが入らないので、シンプルである。</li>
</ul>
</div>
</div>
<div>
課題</div>
<div>
<div>
<ul>
<li>マルチマスターなので不整合に気をつける</li>
<li>repが遅いとなかなか昇格できない。MySQL5.7に期待。</li>
</ul>
</div>
</div>
<div>
<br /></div>
<div>
<br /></div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-15128426543703032092015-10-29T16:57:00.001+09:002015-10-29T17:11:24.868+09:00マイKVM環境を晒しておきます。My Jessie on Jessie KVM environment.皆さん、こんにちは。もうすっかり秋ですね。<br />
最近、寒暖差が激しいので、お体には気をつけてくださいね。<br />
<br />
さてさて、本日は、弊社で使っているDebian Jessie用のKVM環境をご紹介します。<br />
<br />
KVMとはKernel Virtual Machineの略で、Linuxのカーネルで実装された仮想マシンハイパーバイザーのことです。一般的には、ハイパーバイザー自体を指すよりも、それを用いた仮想マシンの方式のことを指す場合も多いかと思います。<br />
<br />
弊社ではKVMによる仮想環境を用い、各種サーバプログラム、WEBクラスターなどの検証を行っています。最近のLinuxディストリビューションでは、すでに簡単な操作で仮想マシンを立ち上げることが可能です。しかし、kvmが最初にバニラカーネルに組み込まれたlinux-2.6.20の頃から自家製スクリプトでkvm仮想マシンを利用しており、それが使い易く、興味を持ってくれる人もいるかもしれないのでここに公開する次第です。<br />
<br />
仮想マシンの抽象化ライブラリであるlibvirtを介さずGUIなども無いので、比較的仕組みが理解しやすくカスタマイズが簡単であることがメリットかと考えています。カーネルの最新機能や最新のqemuに追従することもたやすくできます。<br />
<br />
それでですね、ものはここにおいてあります。<br />
<br />
<a href="https://github.com/ktaka-ccmp/kvm-setup-jessie">https://github.com/ktaka-ccmp/kvm-setup-jessie</a><br />
<br />
Debian jessieで以下のように、<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> sudo apt-get install make aptitude git -y
git clone git@github.com:ktaka-ccmp/kvm-setup-jessie.git
sudo make all </code></pre>
<ol>
<li>gitとmakeとaptitudeをaptでインストール</li>
<li>githubからツールをクローン</li>
<li>make allする</li>
</ol>
<div>
を行えばオッケーです。busyboxとkernelのコンパイル時にmenuconfig画面が表示されますが、通常はそのままExitしてください。<br />
何か思うところがあってカスタマイズしたい場合は、そこでカスタマイズを行ってください。</div>
<br />
仮想マシンの実行は、/kvm/sbin/kvmスクリプトで行います。中身を見てもらえばわかりますが、このスクリプトでやっていることは、qemuコマンドの実行と、unix domainソケットを通してのqemuプロセスの制御です。<br />
<br />
ではまず、v001という仮想マシンを起動してみます。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm create v001
booting v001 ....
</code></pre>
<br />
v001が生きているかどうかは、次のように確認できます。
<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm chk v001
QEMU 2.4.0.1 monitor - type 'help' for more information
(qemu) info status
VM status: running
(qemu)
</code></pre>
<br />
次にv002を起動し、状態を確認してみます。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm create v002
booting v002 ....
root@jessie64:~# kvm chk v002
QEMU 2.4.0.1 monitor - type 'help' for more information
(qemu) info status
VM status: running
(qemu)
</code></pre>
<br />
稼働中の仮想マシンの一覧は、以下のように確認できます。conのカラムはコンソールソケット、monのカラムはモニターソケット、imgのカラムはVMのイメージファイルの状態を表しています。v001の行を見るとv001 o o uとなっていますが、これはコンソール、モニターの両ソケットが接続可能で、イメージファイルが使用中(VMが稼働中)であることを表しています。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm list
id con mon img
test - - -
v001 o o u
v002 o o u
</code></pre>
<br />
今度は、v001のコンソールに接続してみます。コンソールに接続するとログインプロンプトが表示されるのでroot/rootでログインします。rootのパスワードはMakefileにベタ書きしてあり、イメージtemplate作成時に設定されます。コンソール接続を終了するには"Ctrl+]"をタイプします。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm con v001
Debian GNU/Linux 8 v001 ttyS0
v001 login: root
Password:
Linux v001 4.2.4-64kvmg01 #1 SMP Wed Oct 28 03:50:51 UTC 2015 x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
root@v001:~# logout
Debian GNU/Linux 8 v001 ttyS0
v001 login:
</code></pre>
<br />
テンプレートイメージ作成時には、/root/.ssh/authorized_keysもコピーしていますので、ホストのマシンと同じ鍵でsshログインすることが可能です。<br />
<br />
v001のIPアドレスはinitrdの中で172.16.1.1を決め打ちで設定しています。v002の場合は172.16.1.2、v250の場合は172.16.1.250が割り当てられ、v250を上限にしてあります。<br />
これらのホスト名、IPは、セットアップ時にホストの/etc/hostsに追記してあります。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# ssh v001
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Oct 29 16:42:31 2015 from 172.16.1.254
root@v001:~#
</code></pre>
<br />
ゲストマシンからは、ホストマシンのNATを介して外部のネットワークと通信が可能です。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@v001:~# ping yahoo.jp
PING yahoo.jp (183.79.227.111) 56(84) bytes of data.
64 bytes from yjpn110.mobile.vip.ogk.yahoo.co.jp (183.79.227.111): icmp_seq=1 ttl=54 time=13.2 ms
64 bytes from yjpn110.mobile.vip.ogk.yahoo.co.jp (183.79.227.111): icmp_seq=2 ttl=54 time=13.3 ms
64 bytes from yjpn110.mobile.vip.ogk.yahoo.co.jp (183.79.227.111): icmp_seq=3 ttl=54 time=13.9 ms
^C
--- yahoo.jp ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 13.224/13.515/13.986/0.349 ms
</code></pre>
<br />
また、同じホスト上の仮想マシン間で通信することも可能です。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@v001:~# ping v002
PING v002 (172.16.1.2) 56(84) bytes of data.
64 bytes from v002 (172.16.1.2): icmp_seq=1 ttl=64 time=1.33 ms
64 bytes from v002 (172.16.1.2): icmp_seq=2 ttl=64 time=1.89 ms
64 bytes from v002 (172.16.1.2): icmp_seq=3 ttl=64 time=5.51 ms
^C
--- v002 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.331/2.914/5.519/1.856 ms
</code></pre>
<br />
別ホストからは、直接仮想マシンにアクセスできないようになっていますので、別ホスト上の仮想マシンとは通信できません。もしそうしたい場合は、ブリッジとvlanの設定を工夫することで、通信可能になるでしょう。<br />
<br />
仮想マシンを停止したいときは、仮想マシンにログインしpoweroffを実行するか、以下のようにshutdownコマンドを発行します。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm shutdown v001
QEMU 2.4.0.1 monitor - type 'help' for more information
(qemu) system_powerdown
(qemu)
root@jessie64:~# kvm chk v001
2015/10/29 17:04:52 socat[17586] E connect(5, AF=1 "/kvm/monitor/v001.sock", 24): Connection refused
</code></pre>
<br />
kvm listコマンドで見ると、停止しているv001は以下のように見えます。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# kvm list
id con mon img
test - - -
v001 - - -
v002 o o u
</code></pre>
<br />
v001のイメージを完全に消したいときは、仮想マシンが停止した状態で、/kvm/data/v001.imgを消してください。<br />
<br />
だいたい使い方は以上になります。<br />
どうぞ、よろしくお願いいたします。<br />
<br />ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-29389112687704839932015-10-19T01:37:00.001+09:002015-10-19T01:39:07.496+09:00Lessons of Steve Jobs: Guy Kawasaki at TEDxUCSD の要点Guy Kawasakiのもう一つのビデオ"Lessons of Steve Jobs" 2013年UCSDでのTEDx です。<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="270" src="https://www.youtube.com/embed/rWv-KoZnpKw" width="480"></iframe><br />
<br />
<div class="p1">
<span class="s1">May he rest in peace, but may his influence continue to inspire us.</span></div>
<ol class="ol1">
<li class="li1"><span class="s1">Experts are clueless. Listen to your heart.</span></li>
<li class="li1"><span class="s1">Customer cannot usually tell you what they want.</span></li>
<li class="li1"><span class="s1">Get the next curve.</span></li>
<li class="li1"><span class="s1">Big challenges beget the best work.</span></li>
<li class="li1"><span class="s1">Design counts.</span></li>
<li class="li1"><span class="s1">Use big graphics and big fonts.</span></li>
<li class="li1"><span class="s1">Changing your mind is sign of intelligence.</span></li>
<li class="li1"><span class="s1">Value not equal to price.</span></li>
<li class="li1"><span class="s1">A players hire A+ players.</span></li>
<li class="li1"><span class="s1">Real CEOs demo.</span></li>
<li class="li1"><span class="s1">Real entrepreneurs ship.</span></li>
<li class="li1"><span class="s1">Marketing = unique value.</span></li>
</ol>
<div class="p1">
<span class="s1">Bonus: Some things need to be believed to be seen.</span></div>
<div class="p2">
<br />
<span class="s1"></span></div>
<div class="p1">
<span class="s1">I can tell you right now. In heaven Steve Jobs is telling God what to do.</span></div>
<div class="p1">
<span class="s1"><br /></span></div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-39323995195626011562015-10-18T18:00:00.000+09:002015-10-19T00:01:12.149+09:00The art of innovation | Guy Kawasaki | TEDxBerkeley の要点メモ<br />
<div>
こんにちは、今日はいい天気にもかかわらず、自宅に引きこもってSNS眺めたり、Youtube見てました。本当はやらなきゃいけないお仕事があったんですが ^^;)</div>
<div>
<br /></div>
<div>
前からちょくちょく見ていたGuy KawasakiのTalkの一つの「The art of innovation(イノベーションの技術)」分用メモです。書き出しておかないと内容忘れて何度も見る羽目になってしまうので。</div>
<div>
<br /></div>
<iframe allowfullscreen="" frameborder="0" height="270" src="https://www.youtube.com/embed/Mtjatz9r-Vc" width="480"></iframe>
<br />
<div>
<br /></div>
<ol class="ol1">
<li class="li1"><span class="s1"></span><span class="s2">Make meaning as opposed to make money. If you start off with the sole desire to make money, you won’t make money, you won’t make meaning, you won’t change the world, and you’ll probably fail. お金よりもそれをやる意味が大事。</span></li>
<li class="li1"><span class="s2">Make Mantra. Two or three explanation why your meaning should exist. Nike: authentic athletic performance, Fedex: Peace of mind, Wendy: (should be) Healthy fast food. その意味が存在する必要を、2、3、4語のマントラにしておくと良い。</span></li>
<li class="li1"><span class="s2">Jump to the next curve. Great innovation occurs when you get to the next curve. 10%の進歩ではなく次のカーブに乗ることが大事。</span></li>
<li class="li1"><span class="s2">Roll the DICEE. Deep, Intelligent, Complete, Empowering, Elegant 深い進歩、賢さ(マスタングは鍵ごとに最高速がプログラムされいる)、完全さ(レクサス)、可能性を与えるもの(Macbook Air)、エレガントさなどの要素が必要。<a href="http://alvinalexander.com/best-practices/guy-kawasaki-dicee-acronym">参考</a></span></li>
<li class="li1"><span class="s2">Don’t worry be crappy. If you waited for perfect world, you would never ship. 完璧さを求めたらいつまでたっても出荷できない。革新的なプロダクトは多少のクラッピネスを恐れずにまず出荷することが大事。</span></li>
<li class="li1"><span class="s2">Let 100 flowers blossom. One of the reasons why I believe in God is there is no other explanations for Apple’s continued survival than the existence of God. When the customer use your product if they say it’s the desktop publishing machine, haleluya, declare the victory, it is now a desktop publishing machine. Positioning branding ultimately comes down to what the customer decides, not to what you decides. マックはワープロ、スプレッドシート、データベースではダメだったけど、DTPマシンとして成功した。ポジショニング、ブランディングは究極的には、顧客が決めるものである。</span></li>
<li class="li1"><span class="s2">Polarize people.The great product polarize people. Don’t be afraid of polarizing people. 偉大なプロダクトは時に敵を作るが恐れてはいけない。ただし、敵を作れと言っているのではない。</span></li>
<li class="li1"><span class="s2">Churn, baby churn. As soon as you ship the product, you need to start listening people and keep evolving the product. 出荷するまではエキスパートの声を無視しなければいけないが、出荷されたら直ちにユーザーの声に耳を傾け、商品を進化させ続けなければならない。</span></li>
<li class="li1"><span class="s2">Niche yourself. ユニーク、価値の2x2マトリックス、右上を取らなければいけない。</span></li>
<li class="li1"><span class="s2">Perfect your pitch. Customize your introduction. 10 20 30 rules of presentation. 10 slides 20 minutes 30 point fonts. ピッチのTips。イントロを聴衆に合わせてカスタマイズ。10スライド、20分のトーク、30ポイントフォント。聴衆年齢の半分のフォントサイズ。</span></li>
<li class="li1"><span class="s2">Don’t let the bozos grind you down. “I think there is a world of market for maybe five computers. Thomas Watson, IBM, 1943” 成功したbozo達の言葉に騙されるな。</span></li>
</ol>
<div>
<br /></div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-31424448157197972252015-10-15T03:06:00.000+09:002015-11-04T01:23:35.267+09:00debian jessieをdebootstrapでインストールした時のメモこんばんは。<br />
ブログを書くのは苦手だけど、なんでもいいから書いてみようシリーズです。<br />
<br />
会社ではディスクレスのネットブート環境を作ってあって、新しいOSをインストールする時には、まずディスクレスでOSを立ち上げそのOS上でdebootstrapやyum groupinstallなどを使ってファイルシステムの中身を作成することが多いです。あるいはディスクレスでブートして、あらかじめ作成しておいた標準システムをネットワーク越しrsyncやddでコピーしたりすることもあります。<br />
このようにすることで、毎回ブレの少ない、あるいは全くブレのないイメージを作成することができ、非常に便利です。<br />
<br />
今回は、ネットブートした後にDebian jessieをdebootstrapした時のメモです。<br />
<br />
そして想定読者は、未来の自分です。(最近前にやったことをすぐに忘れてしまうので^^;)<br />
<br />
1. ネットブートしたOS上でやること。<br />
apt.h.ccmp.jp:3142がMIRROR元のURLに挟んであるのは、apt-cacher-ngを間に挟んでaptの転送量を節約するためです。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> parted /dev/sda
mkfs.ext4 /dev/sda1
mkswap /dev/sda2
mount /dev/sda1 /mnt/
apt-get install debootstrap
time debootstrap --include=openssh-server,openssh-client,rsync,pciutils,tcpdump,strace,libpam-systemd,ntpdate,openntpd jessie /mnt/ http://apt.h.ccmp.jp:3142/ftp.jp.debian.org/debian
mount -t proc none /mnt/proc/
mount -t devtmpfs none /mnt/dev/
mount -t sysfs none /mnt/sys/
rsync -av ~/.ssh/ /mnt/root/.ssh/
vi /mnt/etc/network/interfaces
vi /mnt/etc/fstab
echo "root:root" | chpasswd --root /mnt/
echo "Asia/Tokyo" > /mnt/etc/timezone
cp /mnt/usr/share/zoneinfo/Japan /mnt/etc/localtime
chroot /mnt/
</code></pre>
<br />
/etc/fstabの中身
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> /dev/sda1 / ext4 defaults 1 1
/dev/sda2 swap swap defaults 0 0
</code></pre>
<br />
/etc/network/interfacesの中身
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
auto lo
iface lo inet loopback
auto eth0
#iface eth0 inet dhcp
iface eth0 inet static
address 192.168.60.31
network 192.168.60.0
broadcast 192.168.63.255
netmask 255.255.252.0
gateway 192.168.60.1
source-directory /etc/network/interfaces.d/
</code></pre>
<br />
2. chroot後にイメージ内でやること<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> apt-cache search linux-image
apt-get install linux-image-3.16.0-4-amd64 <span style="font-family: "arial";">grub-pc</span>
apt-get clean
grub-install /dev/sda
update-grub
</code></pre>
<br />
以上で、jessieのミニマム環境がインストールできます。
リブートしてちゃんと立ち上がればOKです。
<br />
<br />
そんなこんなで、立ち上がったOSの容量は。。。<br />
<br />
<pre style="background: #f0f0f0; border: 1px dashed #CCCCCC; color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> root@jessie64:~# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 107G 585M 101G 1% /
</code></pre>
<br />
うーん、こんなもんかなー。ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-54244603344983577032015-10-14T23:45:00.003+09:002022-12-05T19:18:02.760+09:00pppoe接続のインターフェース名を変更する How to rename pppoe interface name.フレッツ回線などで、linuxルーターからpppoeセッションを張る場合、インターフェース名がppp0、ppp1などとなり、どのプロバイダへの接続なのか判別しづらく不便に感じることがあります。<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); color: black; font-family: arial; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; overflow-wrap: normal; word-wrap: normal;"> # ip add show dev ppp0
8: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1454 qdisc pfifo_fast state UNKNOWN qlen 3
link/ppp
inet x.x.x.x peer x.x.x.x/32 scope global ppp0
valid_lft forever preferred_lft forever
</code></pre>
<br />
ここで、ppp0の部分を任意の名前、例えば接続先のプロバイダ名などにしておけば便利でしょう。Debian Wheezyの場合は次のようにしてやれば、そういうことが可能です。<br />
<br />
/etc/network/interfaces<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; overflow-wrap: normal; word-wrap: normal;"> auto irevo
iface irevo inet ppp
provider irevo
</code></pre>
<div>
<br />
/etc/ppp/peers/irevo
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; overflow-wrap: normal; word-wrap: normal;"> noipdefault
hide-password
lcp-echo-interval 20
lcp-echo-failure 3
connect /bin/true
noauth
persist
mtu 1492
noaccomp
default-asyncmap
linkname "irevo"
plugin rp-pppoe.so eth0.10
user "xxxxx@i-revonet.jp"
</code></pre>
<div>
<br />
/etc/ppp/ip-up.d/02ifrename<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; overflow-wrap: normal; word-wrap: normal;"> #!/bin/bash
ifrename(){
if [ "$LINKNAME" != "" ]; then
ip link set $IFNAME down
ip link set $IFNAME name $LINKNAME
ip link set $LINKNAME up
else
exit
fi
}
if [ "$IFNAME" == "$(/sbin/ip route |grep default | cut -f 3 -d " ")" ]; then
ifrename
/sbin/ip route add default dev $LINKNAME
else
ifrename
fi
</code></pre>
<div>
<br />
試してみると確かにインターフェース名がirevoになっていることがわかりました。<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); color: black; font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; overflow-wrap: normal; word-wrap: normal;"> # ip add show dev irevo
5: irevo: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1454 qdisc pfifo_fast state UNKNOWN qlen 3
link/ppp
inet x.x.x.x peer x.x.x.x/32 scope global irevo
valid_lft forever preferred_lft forever
</code></pre>
<br />
<div class="p1">
Debian Jessie以降ではpppdに以下のページの修正が入っていて、上記の/etc/ppp/peers/irevoで「linkname "irevo"」とあるところを「ifname "irevo"」</div>
などに変更することで、インターフェースの名前を任意に設定することができるそうです。<br />
<br />
<a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=458646">https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=458646</a><br />
<br />
おそらく、こちらの方がpppdが知っている名前を変えずに済み問題が少いでしょう。<br />
早く、ルーターのOSをDebian jessieにしようと思います。<br />
<div class="p2">
<span class="s1"></span></div>
<div class="p2">
<span class="s1"></span></div>
</div>
</div>
</div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-57576534371374309832015-10-03T17:11:00.003+09:002015-10-03T17:13:03.469+09:00デスクトップパソコン新調<div class="separator" style="clear: both; text-align: left;">
みなさん、こんにちは。そろそろブログを書かなきゃなと思いつつ10ヶ月以上もブログ更新していないことに気づきました。もう何でもいいから書いてみようと思います(笑)</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
先日、会社で使っているデスクトップパソコンを新しくしました。パソコンのモデルは、Intelの<a href="http://www.intel.co.jp/content/www/jp/ja/nuc/nuc-kit-dn2820fykh.html">NUC BOXDN2820FYKH0</a>というやつで、<a href="http://ark.intel.com/ja/products/79052/Intel-Celeron-Processor-N2820-1M-Cache-up-to-2_39-GHz">Celeron N2820</a>というデュアルコアHTなCPUが乗っています。このCPUはTDPがわずか7.5Wととても低消費電力で、電気代が安く済むので、各拠点のルーターとしても重宝しています。今回はルータ用に買ったあまりをデスクトップ用に転用しました。</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmz-dspD3dNvEZyK_w8eHIb9SzqjTLp3wa3BOr9ZyvsVkajYJ7o0vNjJQsywLbkoTd0GdnVgLprHcS47rzkCKzD_CveWMu_1SCcS2Dr1TmBHBtKv_kcJn0L8C_RD9RRAMWt-7v1CxvnE4/s1600/14438574284641.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"> <img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmz-dspD3dNvEZyK_w8eHIb9SzqjTLp3wa3BOr9ZyvsVkajYJ7o0vNjJQsywLbkoTd0GdnVgLprHcS47rzkCKzD_CveWMu_1SCcS2Dr1TmBHBtKv_kcJn0L8C_RD9RRAMWt-7v1CxvnE4/s640/14438574284641.jpg" /> </a> </div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
主なインターフェースは、HDMI、1G LAN(Realtek)、usb 2.0 x2 、usb 3.0 x 1などです。他にwifi/bluetooth用のチップも内蔵しています。</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjp8cIKTLMo7HcFHkASlhG0z9PODlireipZ-EFV4_mTAvDB0ZMmqzzoYjAwSfXFO5FWq3jlG1fBJJj7-6Tu0a5BLtipUPmMHRcqVqbibAhMbFJp-GMdG9TmDqIwIddkZyWzay9NtQVMrEs/s1600/14438574196490.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"> <img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjp8cIKTLMo7HcFHkASlhG0z9PODlireipZ-EFV4_mTAvDB0ZMmqzzoYjAwSfXFO5FWq3jlG1fBJJj7-6Tu0a5BLtipUPmMHRcqVqbibAhMbFJp-GMdG9TmDqIwIddkZyWzay9NtQVMrEs/s640/14438574196490.jpg" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
OSはもちろんLinux。今回はDebian jessieをインストールしました。</div>
<div class="separator" style="clear: both; text-align: left;">
最近のLinuxはインストールするだけで、特に苦労もせずXや日本語入力環境が使えるので、非常に助かります。(Macイラねんじゃね?とはいえMacbook Air愛用しておりますが…)</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
備忘録 chrome音が出ない件。</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
以下のコマンドで見ると、HDA Intel PCHというデバイスが搭載されており、ALC283とHDMIの2つの出力デバイスがある。</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<blockquote class="tr_bq" style="clear: both;">
ktaka@jessie:~$ aplay -l<br />
**** List of PLAYBACK Hardware Devices ****<br />
card 0: PCH [HDA Intel PCH], device 0: ALC283 Analog [ALC283 Analog]<br />
Subdevices: 1/1<br />
Subdevice #0: subdevice #0<br />
card 0: PCH [HDA Intel PCH], device 3: HDMI 0 [HDMI 0]<br />
Subdevices: 1/1<br />
Subdevice #0: subdevice #0</blockquote>
<div>
<br /></div>
<div>
次のコマンドで、音が出るかどうか試してみるとちゃんと出る。</div>
<div>
<br /></div>
<blockquote class="tr_bq">
aplay -D plughw:0,3 /usr/share/sounds/alsa/Front_Center.wav </blockquote>
<div>
<br /></div>
<div>
<div>
次のファイルを書き換えXを再起動したところ、chromeでyoutubeなどの音声が聞こえるようになりました。</div>
<blockquote class="tr_bq">
/etc/asound.conf<br />
pcm.!default { type hw; card 0 ; device 3; }<br />
ctl.!default { type hw; card 0 ; device 3; }</blockquote>
</div>
<div>
以上、オチもまとめもないんですけど、</div>
<div>
最近のLinuxはデスクトップとしても簡単に使えるので、皆さん使ってみましょう!</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
ktakahttp://www.blogger.com/profile/10493267763709982250noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-50479279839841695032014-11-03T16:25:00.002+09:002014-11-03T16:27:40.761+09:00今日の逸品こんにちは。三連休の最終日、お天気は快晴で絶好の行楽日和となりましたね!<br />
<div>
<br /></div>
<div>
本当はどこかに出かけるか、外で思いっきりスポーツでもしたいところですが、某代理店さんからお借りしている最新の超高性能デバイスを本日中に返送しないといけないので、休日返上でブログを書きながらレポートしたいと思います(^^;)</div>
<div>
<br /></div>
<div>
まずは、写真をと。。。。じゃじゃーん!今日の逸品はこちらです!</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-2ymZrjpBRMs/VFcCrOYV72I/AAAAAAAAP1Q/xI9exwiWRTs/s1600/IMG_20141028_000418_mini.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-2ymZrjpBRMs/VFcCrOYV72I/AAAAAAAAP1Q/xI9exwiWRTs/s1600/IMG_20141028_000418_mini.jpg" height="180" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
IntelのPCI express typeのSSD DC P3700です。大きなヒートシンクに囲われていて、かっこいいですね!まあ、どんだけ熱くなるんだってお話もありますが。。。</div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-Ew2-N9OZ8oQ/VFcCrBlnUrI/AAAAAAAAP1I/h2faFwZt9Nk/s1600/IMG_20141028_000931_mini.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-Ew2-N9OZ8oQ/VFcCrBlnUrI/AAAAAAAAP1I/h2faFwZt9Nk/s1600/IMG_20141028_000931_mini.jpg" height="180" width="320" /></a></div>
<br />
裏面も、NANDチップがびっしりです。どんだけ大容量なんだ!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-symAIHNmAYY/VFcCrdPRCgI/AAAAAAAAP1M/X0dDHuqDoj4/s1600/IMG_20141028_001036.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-symAIHNmAYY/VFcCrdPRCgI/AAAAAAAAP1M/X0dDHuqDoj4/s1600/IMG_20141028_001036.jpg" height="180" width="320" /></a></div>
<br />
どうやら1.6TBのようです!!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-M_ByvyrLegM/VFcCsKpkjfI/AAAAAAAAP1U/ToQQKXa-ULs/s1600/IMG_20141028_001253.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-M_ByvyrLegM/VFcCsKpkjfI/AAAAAAAAP1U/ToQQKXa-ULs/s1600/IMG_20141028_001253.jpg" height="180" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
こんな風に、サーバのPCIeスロットに装着します。ちょっと、見えづらいかも知れませんが、この1Uサイズのサーバは、PCIeスロットが二段になっていて、上段がP3700、下段がLSI RAIDカードとなっています。</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
インテル® Solid-State Drive DC P3700 シリーズのカタログスペックは、こんな感じです。</div>
<table border="1" cellpadding="5" cellspacing="0">
<tbody>
<tr><td>容量 </td><td>Sequencial Read/Write [MB/s]</td><td>4KB Random Read / Write [IOPS]</td><td>8KB Random Read / Write [IOPS]</td></tr>
<tr><td>400 GB</td><td>2,700 / 1,080</td><td>450,000 / 75,000</td><td>275,000 / 32,000</td></tr>
<tr><td>800 GB</td><td>2,800 / 1,900</td><td>460,000 / 90,000</td><td>285,000 / 45,000</td></tr>
<tr><td>1.6 TB</td><td>2,800 / 1,900</td><td>450,000 / 150,000</td><td>290,000 / 75,000</td></tr>
<tr><td>2.0 TB</td><td>2,800 / 2,000</td><td>450,000 / 175,000</td><td>295,000 / 90,000</td></tr>
</tbody></table>
<br />
<div>
( <a href="http://www.intel.co.jp/content/www/jp/ja/solid-state-drives/intel-ssd-dc-family-for-pcie.html">http://www.intel.co.jp/content/www/jp/ja/solid-state-drives/intel-ssd-dc-family-for-pcie.html</a> こちらのページより抜粋 )</div>
<div>
<br />
シーケンシャルのリード/ライトが2.8GB/s、1.9GB/s、4KBのランダムリード/ライトが45万IOPS、15万IOPS !!<br />
<br />
一気に眠気が吹き飛びます!!<br />
<br />
ちなみに、7200rpmのSATA HDDの場合は、ざっくりとシーケンシャル100MB、ランダム100IOPS程度ですので、どれだけ高性能かおわかりいただけるでしょう。<br />
<br />
ライバルは、もしかしてこれ?<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-aiSb8qe5_Ds/VFcOPWe4NtI/AAAAAAAAP2A/SFiNfj2YliM/s1600/IMG_2038.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-aiSb8qe5_Ds/VFcOPWe4NtI/AAAAAAAAP2A/SFiNfj2YliM/s1600/IMG_2038.JPG" height="240" width="320" /></a></div>
<br />
<br />
使ってみたいですよね?それでは使ってみましょう。このカードは、PCIe SSDの新しい規格NVMe (<a href="http://en.wikipedia.org/wiki/NVM_Express">http://en.wikipedia.org/wiki/NVM_Express</a>)に対応していますので、Linuxカーネルでnvmeドライバがロードされている必要があります。<br />
<br />
バニラカーネルをコンパイルする際には、CONFIG_BLK_DEV_NVME=m などとなるようにします。<br />
<br />
うまく認識されると、/dev/nvmexxxとして、見えるようになります。<br />
<blockquote class="tr_bq">
wheezy64:~# fdisk -l /dev/nvme0n1<br />
Disk /dev/nvme0n1: 1600.3 GB, 1600321314816 bytes<br />
64 heads, 32 sectors/track, 1526185 cylinders, total 3125627568 sectors<br />
Units = sectors of 1 * 512 = 512 bytes<br />
Sector size (logical/physical): 512 bytes / 512 bytes<br />
I/O size (minimum/optimal): 512 bytes / 512 bytes<br />
Disk identifier: 0xc55b79ff<br />
Device Boot Start End Blocks Id System</blockquote>
<div class="p1">
</div>
<br />
<div class="p2">
fdiskでパーティション作成します。</div>
<blockquote>
wheezy64:~# fdisk /dev/nvme0n1<br />
Command (m for help): n<br />
Partition type:<br />
p primary (0 primary, 0 extended, 4 free)<br />
e extended<br />
Select (default p): p<br />
Partition number (1-4, default 1): 1<br />
First sector (2048-3125627567, default 2048):<br />
Using default value 2048<br />
Last sector, +sectors or +size{K,M,G} (2048-3125627567, default 3125627567):<br />
Using default value 3125627567<br />
Command (m for help): p<br />
Disk /dev/nvme0n1: 1600.3 GB, 1600321314816 bytes<br />
64 heads, 32 sectors/track, 1526185 cylinders, total 3125627568 sectors<br />
Units = sectors of 1 * 512 = 512 bytes<br />
Sector size (logical/physical): 512 bytes / 512 bytes<br />
I/O size (minimum/optimal): 512 bytes / 512 bytes<br />
Disk identifier: 0xc55b79ff<br />
Device Boot Start End Blocks Id System<br />
/dev/nvme0n1p1 2048 3125627567 1562812760 83 Linux<br />
Command (m for help): w<br />
The partition table has been altered!<br />
Calling ioctl() to re-read partition table.<br />
Syncing disks.</blockquote>
<div class="p2">
</div>
ファイルシステム作ります。<br />
<blockquote class="tr_bq">
wheezy64:~# time mkfs.ext4 /dev/nvme0n1p1<br />
mke2fs 1.42.5 (29-Jul-2012)<br />
Discarding device blocks: done <br />
Filesystem label=<br />
OS type: Linux<br />
Block size=4096 (log=2)<br />
Fragment size=4096 (log=2)<br />
Stride=0 blocks, Stripe width=0 blocks<br />
97681408 inodes, 390703190 blocks<br />
19535159 blocks (5.00%) reserved for the super user<br />
First data block=0<br />
Maximum filesystem blocks=4294967296<br />
11924 block groups<br />
32768 blocks per group, 32768 fragments per group<br />
8192 inodes per group<br />
Superblock backups stored on blocks:<br />
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,<br />
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,<br />
102400000, 214990848<br />
Allocating group tables: done <br />
Writing inode tables: done <br />
Creating journal (32768 blocks): done<br />
Writing superblocks and filesystem accounting information: done <br />
<br />
real 0m13.304s<br />
user 0m2.230s<br />
sys 0m0.440s</blockquote>
マウントします。<br />
<blockquote class="tr_bq">
wheezy64:~# mkdir /mnt/p3700<br />
wheezy64:~# mount /dev/nvme0n1p1 /mnt/p3700/ </blockquote>
<blockquote class="tr_bq">
wheezy64:~# df -h<br />
Filesystem Size Used Avail Use% Mounted on<br />
rootfs 48G 510M 47G 2% /<br />
tmpfs 48G 510M 47G 2% /<br />
tmpfs 9.5G 228K 9.5G 1% /run<br />
tmpfs 5.0M 0 5.0M 0% /run/lock<br />
tmpfs 10M 0 10M 0% /dev<br />
tmpfs 19G 0 19G 0% /run/shm<br />
/dev/nvme0n1p1 1.5T 70M 1.4T 1% /mnt/p3700 </blockquote>
とりあえず、100GB程度、読み書きしてみます。まずは、Write。<br />
<blockquote class="tr_bq">
wheezy64:~# time dd if=/dev/zero of=/mnt/p3700/hello bs=100M count=1000 oflag=direct<br />
1000+0 records in<br />
1000+0 records out<br />
104857600000 bytes (105 GB) copied, 77.9616 s, <span style="font-size: x-large;"><b>1.3 GB/s</b></span><br />
real 1m17.964s<br />
user 0m0.000s<br />
sys 0m45.700s </blockquote>
</div>
<div>
そして、Read。<br />
<blockquote class="tr_bq">
wheezy64:~# time dd of=/dev/null if=/mnt/p3700/hello bs=100M count=1000 iflag=direct<br />
1000+0 records in<br />
1000+0 records out<br />
104857600000 bytes (105 GB) copied, 40.6814 s, <span style="font-size: x-large;"><b>2.6 GB/s</b></span><br />
real 0m40.684s<br />
user 0m0.010s<br />
sys 0m22.650s</blockquote>
カタログスペックには若干届かないですけど、爆速です!!<br />
<br />
大事なのでもう一度、<b><span style="font-size: x-large;">爆速です!!</span></b><br />
<br />
ランダムIOを測ってみます。まずはrandomwrite。<br />
<blockquote class="tr_bq">
wheezy64:~# fio --filename=/mnt/p3700/hello --direct=1 --rw=randwrite --bs=4k --size=2G --numjobs=64 --runtime=180 --name=file1 --ioengine=aio --iodepth=512 --group_reporting<br />
file1: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=512<br />
...<br />
file1: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=512<br />
2.0.8<br />
Starting 64 processes<br />
Jobs: 1 (f=1): [____________________________________________________________w___] [98.4% done] [0K/833.5M /s] [0 /213K iops] [eta 00m:03s]s]<br />
file1: (groupid=0, jobs=64): err= 0: pid=20537<br />
write: io=129218MB, bw=734950KB/s, iops=183737 , runt=180039msec<br />
slat (usec): min=3 , max=2162.1K, avg=322.56, stdev=12338.02<br />
clat (usec): min=12 , max=9825.2K, avg=166237.67, stdev=381829.66<br />
lat (usec): min=20 , max=9825.2K, avg=166560.46, stdev=382293.33<br />
clat percentiles (msec):<br />
| 1.00th=[ 5], 5.00th=[ 6], 10.00th=[ 7], 20.00th=[ 8],<br />
| 30.00th=[ 9], 40.00th=[ 11], 50.00th=[ 13], 60.00th=[ 22],<br />
| 70.00th=[ 70], 80.00th=[ 208], 90.00th=[ 519], 95.00th=[ 898],<br />
| 99.00th=[ 1844], 99.50th=[ 2343], 99.90th=[ 3458], 99.95th=[ 3884],<br />
| 99.99th=[ 5211]<br />
bw (KB/s) : min= 0, max=310600, per=1.82%, avg=13395.80, stdev=22803.92<br />
lat (usec) : 20=0.01%, 50=0.01%, 100=0.01%, 250=0.01%, 500=0.01%<br />
lat (usec) : 750=0.01%, 1000=0.01%<br />
lat (msec) : 2=0.01%, 4=0.02%, 10=39.48%, 20=19.46%, 50=8.79%<br />
lat (msec) : 100=4.69%, 250=9.58%, 500=7.63%, 750=3.74%, 1000=2.47%<br />
lat (msec) : 2000=3.33%, >=2000=0.79%<br />
cpu : usr=0.92%, sys=13.35%, ctx=29141732, majf=0, minf=3026598<br />
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%<br />
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%<br />
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.1%<br />
issued : total=r=0/w=33079899/d=0, short=r=0/w=0/d=0<br />
Run status group 0 (all jobs):<br />
WRITE: io=129218MB, aggrb=734949KB/s, minb=734949KB/s, maxb=734949KB/s, mint=180039msec, maxt=180039msec<br />
Disk stats (read/write):<br />
nvme0n1: ios=0/33078988, merge=0/0, ticks=0/5930480, in_queue=5933370, util=99.60%</blockquote>
<br />
そしてRandomRead。<br />
<blockquote>
wheezy64:~# fio --filename=/mnt/p3700/hello --direct=1 --rw=randread --bs=4k --size=2G --numjobs=64 --runtime=180 --name=file1 --ioengine=aio --iodepth=512 --group_reporting<br />
file1: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=512<br />
...<br />
file1: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=512<br />
2.0.8<br />
Starting 64 processes<br />
Jobs: 12 (f=6): [r__r____r___r__r__________r_r_r_r________________________r_r_r__] [93.2% done] [1403M/0K /s] [359K/0 iops] [eta 00m:08s]]<br />
file1: (groupid=0, jobs=64): err= 0: pid=21635<br />
read : io=131072MB, bw=1194.3MB/s, iops=305724 , runt=109754msec<br />
slat (usec): min=1 , max=104416 , avg=181.86, stdev=2273.95<br />
clat (usec): min=88 , max=1698.4K, avg=94142.32, stdev=89737.06<br />
lat (usec): min=102 , max=1698.5K, avg=94324.37, stdev=89854.71<br />
clat percentiles (msec):<br />
| 1.00th=[ 17], 5.00th=[ 27], 10.00th=[ 32], 20.00th=[ 37],<br />
| 30.00th=[ 42], 40.00th=[ 57], 50.00th=[ 72], 60.00th=[ 85],<br />
| 70.00th=[ 102], 80.00th=[ 131], 90.00th=[ 180], 95.00th=[ 241],<br />
| 99.00th=[ 478], 99.50th=[ 594], 99.90th=[ 865], 99.95th=[ 979],<br />
| 99.99th=[ 1270]<br />
bw (KB/s) : min= 6, max=164272, per=1.77%, avg=21641.11, stdev=11545.90<br />
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%<br />
lat (msec) : 2=0.01%, 4=0.03%, 10=0.40%, 20=1.29%, 50=34.99%<br />
lat (msec) : 100=32.37%, 250=26.29%, 500=3.75%, 750=0.66%, 1000=0.15%<br />
lat (msec) : 2000=0.05%<br />
cpu : usr=1.34%, sys=37.50%, ctx=2798932, majf=0, minf=1003485<br />
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%<br />
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%<br />
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.1%<br />
issued : total=r=33554432/w=0/d=0, short=r=0/w=0/d=0<br />
Run status group 0 (all jobs):<br />
READ: io=131072MB, aggrb=1194.3MB/s, minb=1194.3MB/s, maxb=1194.3MB/s, mint=109754msec, maxt=109754msec<br />
Disk stats (read/write):<br />
nvme0n1: ios=33536051/4, merge=0/0, ticks=4207010/0, in_queue=4260150, util=100.00%</blockquote>
レポートから結果を抜粋してみると。 <span style="font-size: x-large;"><b>write 183,737iops、read 305,724iops</b></span>とこれも<span style="font-size: x-large;"><b>爆速</b></span><b><span style="font-size: x-large;">です!!</span> </b> </div>
<div>
<br /></div>
<div>
最後にざっくりとmysqlのベンチマークを実行してみます。</div>
<div>
<br /></div>
<div>
Intel SSD DC P3700の場合</div>
<div>
MySQLバージョン: "5.5.38"</div>
<div>
<blockquote class="tr_bq">
root@kvm3:~# time mysqlslap --concurrency=50 --iterations=1 --auto-generate-sql --engine=innodb --auto-generate-sql-load-type=write --number-of-queries=5000000 --port=3306 --host=197.3.o -proot<br />
Benchmark<br />
Running for engine innodb<br />
Average number of seconds to run all queries: 153.368 seconds<br />
Minimum number of seconds to run all queries: 153.368 seconds<br />
Maximum number of seconds to run all queries: 153.368 seconds<br />
Number of clients running queries: 50<br />
Average number of queries per client: 100000<br />
<br />
real<span class="Apple-tab-span"> </span>2m33.485s<br />
user<span class="Apple-tab-span"> </span>0m38.288s<br />
sys<span class="Apple-tab-span"> </span>4m37.968s</blockquote>
</div>
<div>
5,000,000クエリーを213.485秒で処理したので、約23,420クエリー/秒の処理性能でした。</div>
<div>
<br /></div>
<div>
ちなみに、AWSのRDSの場合は以下の通りでした。</div>
<div>
<br /></div>
<div>
<div>
インスタンスタイプ: db.t2.medium</div>
<div>
MySQLバージョン: "5.6.17"</div>
<div>
ストレージサイズ: 10GB</div>
</div>
<div>
<blockquote class="tr_bq">
root@aws2:~# time mysqlslap --concurrency=50 --iterations=1 --auto-generate-sql --engine=innodb --auto-generate-sql-load-type=write --number-of-queries=5000000 --port=3306 --host=test.hsjektsilal9.ap-northeast-1.rds.amazonaws.com -p -vv<br />
Building Create Statements for Auto<br />
Building Query Statements for Auto<br />
Generating INSERT Statements for Auto<br />
Parsing engines to use.<br />
Starting Concurrency Test<br />
Loading Pre-data<br />
Generating primary key list<br />
Generating stats<br />
Benchmark<br />
Running for engine innodb<br />
Average number of seconds to run all queries: 1082.128 seconds<br />
Minimum number of seconds to run all queries: 1082.128 seconds<br />
Maximum number of seconds to run all queries: 1082.128 seconds<br />
Number of clients running queries: 50<br />
Average number of queries per client: 100000<br />
<br />
real 18m2.874s<br />
user 0m8.965s<br />
sys 0m46.891s</blockquote>
<div>
<br /></div>
<div>
5,000,000クエリーを1082.128秒で処理したので、約4,620クエリー/秒の処理性能でした。</div>
</div>
<div>
<br /></div>
<div>
これも<span style="font-size: x-large;"><b>爆速</b></span>といえるでしょう!!</div>
<div>
<br />
<span style="font-size: large;">MySQL用のストレージとして、Intel DC P3700いかがでしょうか?</span><br />
<br /></div>
<div>
現場からは以上です!</div>
<div>
<br /></div>
ktakahttp://www.blogger.com/profile/17802956090186648544noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-46715812911691852032014-10-26T21:51:00.001+09:002014-10-26T21:51:58.287+09:00今日のお仕事<div class="separator" style="clear: both; text-align: left;">
こんにちは。昨日、今日と暖かく過ごしやすい週末でしたね。</div>
<div class="separator" style="clear: both; text-align: left;">
とはいえ、弊社では、サーバの納期が迫っていたため、休日返上で準備に追われていました。</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
今回のサーバはこんなやつです。</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-Qiya6Zfk3H0/VEx2IZyP-YI/AAAAAAAAPfw/IDu2VHxF8mA/s1600/IMG_4850s.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-Qiya6Zfk3H0/VEx2IZyP-YI/AAAAAAAAPfw/IDu2VHxF8mA/s1600/IMG_4850s.jpg" height="240" width="320" /></a></div>
<br />
3Uのシャーシに3.5インチのHDDベイが16個並んでいて、一見ストレージサーバのように見えます。しかし、背面をのぞいてみると...<br />
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-SaKha_CKH0g/VEx2Inpg0dI/AAAAAAAAPfo/cNtFCjZe3OE/s1600/IMG_5021s.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-SaKha_CKH0g/VEx2Inpg0dI/AAAAAAAAPfo/cNtFCjZe3OE/s1600/IMG_5021s.jpg" height="240" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
こんな、1ソケットのXeonサーバボードが....</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-X-NmphLnibo/VEx2HwrurOI/AAAAAAAAPfg/SbNfcdO0E1A/s1600/IMG_4856s.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-X-NmphLnibo/VEx2HwrurOI/AAAAAAAAPfg/SbNfcdO0E1A/s1600/IMG_4856s.jpg" height="240" width="320" /></a></div>
<br />
こんな感じで、8ノード搭載されています!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-Hgr0swmWTfM/VEx2H5F8ETI/AAAAAAAAPfc/xe3Qf9w3cSY/s1600/IMG_4851s.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-Hgr0swmWTfM/VEx2H5F8ETI/AAAAAAAAPfc/xe3Qf9w3cSY/s1600/IMG_4851s.jpg" height="240" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
IPMIも使えて、ssh経由のSMASH/CLPもなかなかグッドです。<br />
<br />
WEBやキャッシュサーバなんかには、最適なんじゃないかと思います。<br />
<br />
休日にも関わらず、出荷のお手伝いをしてくれたスタッフのMさん、ありがとう!ktakahttp://www.blogger.com/profile/17802956090186648544noreply@blogger.com0tag:blogger.com,1999:blog-4279971486311743746.post-7499310550348456362014-08-27T14:31:00.003+09:002014-08-27T21:29:16.436+09:00出荷を待つサーバたち皆さんこんばんは。
今日は肌寒い一日でしたね。<br />
季節の変り目には体調を崩しがちですので注意しましょうね。
<br />
<br />
さて以下の写真は弊社で販売しているIntel IvyBridge Xeonプロセッサを2基搭載した、とっても高性能なサーバたちです。
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-DcnvkhMp9XI/U_1rgMmJUrI/AAAAAAAAOhk/pDngBY1nBaA/s1600/1402285918006.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-DcnvkhMp9XI/U_1rgMmJUrI/AAAAAAAAOhk/pDngBY1nBaA/s1600/1402285918006.jpg" height="180" width="320" /></a></div>
<br />
メモリは、最大で512GB搭載可能で、最近は100GBを超えるものがよくご注文いただきます。LSIの高性能なRAIDカードが標準装備。8本のSSDと組み合わせることで、びっくりする位のIO性能をたたき出します。<br />
<br />
オンラインゲームなどを支えるデータベースの高速化にぴったりです。
<br />
<br />
お好みにより、10GBaseTのネットワークにも対応可能です。
商品紹介ページは<a href="http://clustcom.com/products/345-2014-ivybridge-ep-xeon-dual-cpu-1u-e5-2600-v2">こちら</a><br />
<br />
一家に一台。奥さんいかがですか!<br />
<br />
追記、開腹写真もありましたので、貼っておきます。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-75b5zcC8JC4/U_3PCHfN3gI/AAAAAAAAOh0/IJD1-4LBslI/s1600/DSC_0104.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-75b5zcC8JC4/U_3PCHfN3gI/AAAAAAAAOh0/IJD1-4LBslI/s1600/DSC_0104.JPG" height="180" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<br />ktakahttp://www.blogger.com/profile/17802956090186648544noreply@blogger.com0