j.mohttps://justinmontgomery.com/2019-10-23T23:30:00-07:00Ingesting JSON Logs with Nginx and DataDog2019-10-23T23:30:00-07:002019-10-23T23:30:00-07:00Justin Montgomerytag:justinmontgomery.com,2019-10-23:/ingesting-json-logs-with-nginx-and-datadog<p>I wanted to convert my Nginx logs to JSON and was hoping to utilize DataDog's built-in pipelines and parsers to ingest them without heavy (or any) customization. Indeed DataDog even <a href="https://www.datadoghq.com/blog/how-to-monitor-nginx-with-datadog/#use-json-logs-for-automatic-parsing">wrote a guide</a> on how to do it! Unfortunately the guide sent me in the completely wrong direction since it gives an <code>nginx.conf</code> log format which DataDog's <em>own systems</em> will not natively make searchable. Sure it will parse the JSON into a pretty tree to display, but not much more than that. <a href="https://medium.com/zendesk-engineering/datadog-log-management-from-zero-to-one-3a5a5675dff9">This guide</a> from Zendesk Engineering was much more useful.</p>
<p>It took a lot of trial and error but I think I finally got an Nginx <code>log_format</code> configuration that DataDog will natively ingest requiring no customizations in their dashboard. Customizing the ingestion is powerful but it's fragile and then you forget you ever did it when something breaks or changes in the future.</p>
<p>To enable this we need to name our log fields following DataDog's <a href="https://docs.datadoghq.com/logs/processing/attributes_naming_convention/">Standard Attributes</a> guide. We can still add custom values, such as I did for <code>x_forwarded_for</code>, and create a <a href="https://docs.datadoghq.com/logs/explorer/?tab=logsearch#setup">facet</a> in the DataDog dashboard to be able to filter on those custom values. I just wanted to avoid as much of that as possible.</p>
<p>Another feature I wanted was millisecond precision <em>directly from the timestamp in the Nginx log</em>. By default DataDog displays <a href="https://docs.datadoghq.com/logs/faq/why-do-my-logs-not-have-the-expected-timestamp/">its own timestamp</a> appended by the <code>datadog-agent</code> running on the server which can differ from the logged value. However neither the <code>$time_local</code> or <code>$time_iso8601</code> variables of Nginx include milliseconds, but <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#var_msec"><code>$msec</code></a> does!</p>
<p>Alas <code>$msec</code> emits a 10.3 <code>epoch.millisecond</code> format, while DataDog <a href="https://docs.datadoghq.com/logs/processing/processors/?tab=ui#log-date-remapper">only supports</a> the 13 digit millisecond epoch format. I combined a couple solutions I found on online to create a <code>map</code> which concats the 10.3 format into the 13 digit format we need.</p>
<p>Putting all the pieces together, we end up with:</p>
<div class="highlight"><pre><span></span><code>http {
...
map $msec $msec_no_decimal { ~(.*)\.(.*) $1$2; }
log_format json_datadog escape=json
'{'
'"timestamp":"$msec_no_decimal",'
'"http":{'
'"method":"$request_method",'
'"request_id":"$request_id",'
'"status_code":$status,'
'"content_type":"$content_type",'
'"useragent":"$http_user_agent",'
'"referrer":"$http_referer",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"url":"$request_uri"'
'},'
'"network":{'
'"bytes_written":$bytes_sent,'
'"bytes_read":$request_length,'
'"client":{'
'"ip":"$remote_addr",'
'"port":$remote_port'
'},'
'"destination":{'
'"ip":"$server_addr",'
'"port":$server_port'
'},'
'"nginx":{'
'"request_time":$request_time,'
'"upstream_connect_time":$upstream_connect_time,'
'"upstream_response_time":$upstream_response_time,'
'"upstream_header_time":$upstream_header_time'
'}'
'}'
'}';
...
}
server {
...
access_log /var/log/nginx/radsite-access.log json_datadog;
...
}
</code></pre></div>
<p>Which creates output in the DataDog dashboard like:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">"http"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"status_code"</span><span class="p">:</span><span class="mi">200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"status_category"</span><span class="p">:</span><span class="s2">"OK"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"content_type"</span><span class="p">:</span><span class="s2">"application/json"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"referrer"</span><span class="p">:</span><span class="s2">""</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"url"</span><span class="p">:</span><span class="s2">"/some/path?user_id=20k40ffk"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"url_details"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"path"</span><span class="p">:</span><span class="s2">"/some/path"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"queryString"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"user_id"</span><span class="p">:</span><span class="s2">"20k40ffk"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"method"</span><span class="p">:</span><span class="s2">"GET"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"request_id"</span><span class="p">:</span><span class="s2">"d4f70c20f1c9cf8263753e601c0f3594"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"useragent"</span><span class="p">:</span><span class="s2">"CFNetwork/976 Darwin/18.2.0"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"useragent_details"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"os"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"family"</span><span class="p">:</span><span class="s2">"iOS"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"major"</span><span class="p">:</span><span class="s2">"12"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"browser"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"family"</span><span class="p">:</span><span class="s2">"safari"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"major"</span><span class="p">:</span><span class="s2">"3"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"device"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"family"</span><span class="p">:</span><span class="s2">"iOS-Device"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"model"</span><span class="p">:</span><span class="s2">"iOS-Device"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"category"</span><span class="p">:</span><span class="s2">"Mobile"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"brand"</span><span class="p">:</span><span class="s2">"Apple"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"x_forwarded_for"</span><span class="p">:</span><span class="s2">"1.1.2.2"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"network"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"bytes_written"</span><span class="p">:</span><span class="mi">928</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"bytes_read"</span><span class="p">:</span><span class="mi">879</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"client"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="s2">"10.10.10.2"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="mi">30020</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"destination"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="mi">443</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="s2">"10.10.10.3"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"nginx"</span><span class="p">:{</span>
<span class="w"> </span><span class="nt">"request_time"</span><span class="p">:</span><span class="mf">0.025</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"upstream_connect_time"</span><span class="p">:</span><span class="mf">0.004</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"upstream_header_time"</span><span class="p">:</span><span class="mf">0.024</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"upstream_response_time"</span><span class="p">:</span><span class="mf">0.024</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"timestamp"</span><span class="p">:</span><span class="s2">"1571897461960"</span>
<span class="p">}</span>
</code></pre></div>Dropbox in Docker with systemd2018-10-23T13:31:00-07:002018-10-23T13:31:00-07:00Justin Montgomerytag:justinmontgomery.com,2018-10-23:/dropbox-in-docker-with-systemd<p>My CentOS 7 headless install of Dropbox recently quit syncing due to the <a href="https://www.dropbox.com/help/desktop-web/system-requirements?oref=e#linux">new Dropbox linux requirements</a>. Rather than try to hack it back together I found this great guide on <a href="https://valh.io/blog/Dockerize-Dropbox-on-Linux">running Dropbox in a Docker container</a> which utilizes a <a href="https://hub.docker.com/r/janeczku/dropbox/">pre-built container</a> that can update itself, assuming Dropbox puts any more effort into their linux client. Cough. You can audit the security/operation of the container via its <a href="https://github.com/janeczku/docker-dropbox/blob/master/Dockerfile">Dockerfile</a>.</p>
<p>However one thing both the guide docs and container docs above leave out is automating operation of the Dropbox container via systemd, which of course is the default init system most places nowadays including my CentOS 7 and Ubuntu 18.04 bionic servers. The error I kept encountering was:</p>
<blockquote>
<p>The name "dropbox" is already in use by container f9e5798a82e9</p>
</blockquote>
<p>This is because all the guides suggest using the Docker parameter <code>--name=dropbox</code> which will only succeed the first time you issue the <code>docker run</code> command. When you restart the container via <code>run</code> instead of <code>start</code> it instructs docker to <a href="https://stackoverflow.com/a/46138763/1107232">build a new container</a>. Rebuilding the container is also when the Dockerfile would update to the latest version of Dropbox so we can't simply switch to <code>start</code>.</p>
<p>To get around this we use the systemd directive <code>ExecStartPre=-/usr/bin/docker rm dropbox</code> to remove any previous containers using the name <code>dropbox</code>, and additionally prefix the command with a <code>-</code> which tells systemd to ignore any failures reported by the command. ie: if there is no container to remove docker reports an error we don't care about.</p>
<ol>
<li>Update the unit file fields:<br>
<code>MY_USERNAME</code>, <code>MY_UID</code>, and <code>MY_GID</code></li>
<li>Drop it into:<br>
<code>/etc/systemd/system/dropbox.service</code></li>
<li>Reload, enable, and start Dropbox:<br>
<code>systemctl daemon-reload && systemctl enable dropbox && systemctl start dropbox</code></li>
</ol>
<p>This is working on my Ubuntu bionic box and I always welcome improvements, just comment on the gist!</p>
<script src="https://gist.github.com/jmooo/c3f211a48292dd1aeb176ec258e32837.js"></script>Wordpress on Google Cloud SQL2017-06-19T16:39:00-07:002017-06-22T14:33:00-07:00Justin Montgomerytag:justinmontgomery.com,2017-06-19:/wordpress-on-google-cloud-sql<p>In trying to get a Wordpress install migrated to Google Cloud I ran into a frustrating limitation, that Cloud SQL instances (the Google equivalent of AWS RDS) are not on your private network! There is a <a href="https://googlecloudplatform.uservoice.com/forums/302613-cloud-sql/suggestions/17249429-mysql-instances-should-also-have-a-private-network">feature request</a> to move Cloud SQL onto your private network, as most other Google Cloud services are, but the complaints go back over 3 years since the service was in beta so I'm not holding my breath. The only comment from Google I can find is:</p>
<blockquote>
<p>There are some technical reasons why this is not possible currently, but we hope we can do it in the future, it’s a popular request. - Paul Nash (Product Manager, GCE) Nov 27, 2016</p>
</blockquote>
<p>This leaves two options, the first is to and get Wordpress to connect to the Cloud SQL instance via public IPv4 and use SSL certificates for encryption. However MySQL SSL certs are quagmire, Wordpress just isn't designed to support certs for database access. Most articles on using SSL require code hacks to core Wordpress files that would be fragile and break during upgrades. There is a <a href="https://core.trac.wordpress.org/ticket/28625">3 year old ticket</a> to add SSL support to db connections, but again, I'm not holding my breath. The most telling status update on that ticket:</p>
<blockquote>
<p>Milestone changed from 4.5 to Future Release</p>
</blockquote>
<p>The second option is using the <a href="https://cloud.google.com/sql/docs/mysql/connect-external-app#proxy">Cloud SQL proxy</a>, which is equally a pain in the ass to setup and maintain through inevitable fragility, but at least Wordpress just thinks it is connecting to a local MySQL instance. The proxy traffic will still be encrypted with SSL certificates, but they are automatically handled by the proxy app and renewed hourly.</p>
<p>Expanding on Google's decent-but-unecessarily-complicated <a href="https://cloud.google.com/sql/docs/mysql/connect-external-app#proxy">proxy guide</a>:</p>
<p>1) <a href="https://console.cloud.google.com/flows/enableapi?apiid=sqladmin&redirect=https://console.cloud.google.com">Enable the Cloud SQL API</a> from within the GCE interface</p>
<p>2) Install the proxy client, I copied it to: <code>/opt/gcp/cloud_sql_proxy</code></p>
<p>3) Authentication, there <a href="https://cloud.google.com/sql/docs/mysql/sql-proxy#authentication-options">are a few options</a> but since the Wordpress install will be on Google Compute Engine within the same project as our Cloud SQL instance, I'll just let the instance service account provide authentication. A major caveat, Google claims:</p>
<blockquote>
<p>the default service account for the Compute Engine instance has the necessary permissions for authenticating the proxy</p>
</blockquote>
<p>But this was definitely <em>not</em> true for me. In fact the <strong>Cloud API access scopes</strong> under my GCE instance details specifically said <em>No Cloud SQL access</em>. Instead I had to create a new service account with proper permissions in the next step.</p>
<p>4) <a href="https://console.cloud.google.com/iam-admin/serviceaccounts">Create a new service account</a> called <code>wordpress-cloudsql</code> and give it the <code>Cloud SQL Client</code> role.</p>
<p>5) <a href="https://cloud.google.com/sql/docs/mysql/sql-proxy#instances-options">Specify instances for the proxy</a>, again there are several options but I'm using the most direct: the <code>-instances</code> flag. Use the fully qualified name of your Cloud SQL instance, ex: <code>example-project:us-west1:prod-db-1</code>.</p>
<p>6) Start the proxy. For ease I'm going to use tcp instead of unix sockets, but if you want to setup sockets Wordpress <a href="https://codex.wordpress.org/Editing_wp-config.php#MySQL_Sockets_or_Pipes">handles them fine</a>.</p>
<p>Test everything is working via the cli:</p>
<p><code>./cloud_sql_proxy -instances=example-project:us-west1:prod-db-1=tcp:3306</code></p>
<p>Hopefully you seem something similar to this:</p>
<div class="highlight"><pre><span></span><code><span class="mf">6</span><span class="o">/</span><span class="mf">20</span><span class="w"> </span><span class="mf">15</span><span class="p">:</span><span class="mf">51</span><span class="p">:</span><span class="mf">48</span><span class="w"> </span><span class="kr">List</span><span class="n">ening</span><span class="w"> </span><span class="kr">on</span><span class="w"> </span><span class="mf">127.0.0.1</span><span class="p">:</span><span class="mf">3306</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="n">example</span><span class="o">-</span><span class="n">project</span><span class="p">:</span><span class="n">us</span><span class="o">-</span><span class="n">west1</span><span class="p">:</span><span class="n">prod</span><span class="o">-</span><span class="n">db</span><span class="o">-</span><span class="mf">1</span>
<span class="mf">6</span><span class="o">/</span><span class="mf">20</span><span class="w"> </span><span class="mf">15</span><span class="p">:</span><span class="mf">51</span><span class="p">:</span><span class="mf">48</span><span class="w"> </span><span class="kr">Read</span><span class="n">y</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="kr">new</span><span class="w"> </span><span class="n">connections</span>
</code></pre></div>
<p>Nice. To automate it on CentOS 7 here is my systemd unit file:</p>
<div class="highlight"><pre><span></span><code><span class="k">[Unit]</span>
<span class="na">Description</span><span class="o">=</span><span class="s">Google Cloud Compute Engine SQL Proxy (until GCP DB servers are allowed on private network)</span>
<span class="na">After</span><span class="o">=</span><span class="s">network.target</span>
<span class="k">[Service]</span>
<span class="na">Type</span><span class="o">=</span><span class="s">simple</span>
<span class="na">User</span><span class="o">=</span><span class="s">root</span>
<span class="na">Group</span><span class="o">=</span><span class="s">root</span>
<span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
<span class="na">ExecStart</span><span class="o">=</span><span class="s">/opt/gcp/cloud_sql_proxy </span>\
<span class="w"> </span><span class="s">-instances=example-project:us-west1:prod-db-1=tcp:3306</span>
<span class="k">[Install]</span>
<span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span>
</code></pre></div>
<p>7) Update <code>wp-config.php</code> on your GCE instance to point at localhost, since the whole point of the proxy is to make Cloud SQL available locally:</p>
<div class="highlight"><pre><span></span><code><span class="o">/**</span><span class="w"> </span><span class="n">MySQL</span><span class="w"> </span><span class="n">hostname</span><span class="w"> </span><span class="o">*/</span>
<span class="n">define</span><span class="p">(</span><span class="s">'DB_HOST'</span><span class="p">,</span><span class="w"> </span><span class="s">'127.0.0.1'</span><span class="p">);</span>
</code></pre></div>
<p>This proxy method also has the benefit of accessing Cloud SQL via your <a href="https://cloud.google.com/solutions/connecting-securely#bastion">bastion host</a> instead of with SSL certs. For ex using <a href="https://www.sequelpro.com/">Sequel Pro</a> on macOS choose "SSH Connection" and put your Wordpress GCE instance as <code>ssh_host</code> (which hopefully you're already using <a href="http://blog.scottlowe.org/2015/11/21/using-ssh-bastion-host/">ProxyCommand</a> to connect thru your bastion with) and set the <code>mysql_host</code> as <code>127.0.0.1</code>. Also, in the Cloud SQL dashboard remove every <em>authorized network</em> you may have previously added. Since all the connections are happening inside GCP so you don't need any public ips whitelisted.</p>
<p>That is a whole lotta mess to go through when on AWS you can just contact RDS directly over your private, already encrypted network. I look forward to the day Cloud SQL instances are on your private network and I can put a giant "NO LONGER REQUIRED, PRAISE BE" banner on the top of this page.</p>Add Cancel and Delete buttons to django-crispy-forms2017-05-10T10:32:00-07:002017-05-10T10:32:00-07:00Justin Montgomerytag:justinmontgomery.com,2017-05-10:/add-cancel-and-delete-buttons-to-django-crispy-forms<p>I've been using <a href="http://django-crispy-forms.readthedocs.io/">django-crispy-forms</a> with <a href="https://v4-alpha.getbootstrap.com/">Bootstrap 4</a> on a little Django project which has saved me a ton of manual Bootstrap formatting effort. One tiny issue I came across was crispy forms has nice <code>Submit</code> and <code>Button</code> <a href="http://django-crispy-forms.readthedocs.io/en/latest/layouts.html#bootstrap-layout-objects">objects</a> to add buttons to forms. However I wanted a Cancel and Delete button on my <code>UpdateView</code> but using the <code>Button</code> object will cause the form to POST which isn't what I want.</p>
<p>The simplest solution seems to be directly using crispy's <code>HTML</code> object, described aptly in the docs:</p>
<blockquote>
<p>HTML: A very powerful layout object. Use it to render pure html code. In fact it behaves as a Django template and it has access to the whole context of the page where the form is being rendered:</p>
<p>HTML("{% if success %} <p>Operation was successful</p> {% endif %}")</p>
</blockquote>
<p>Access to the whole template context? Yes please! Specifically for the delete button we need to pass a parameter, the <code>object.id</code> of the current object, to the delete route. Additionally the delete button is wrapped in an <code>{% if object %}</code> tag to only display if there is an existing form to delete. For example if you reuse the form template for your <code>CreateView</code> then accessing <code>object</code> will throw an error, it doesn't exist yet!</p>
<p><strong><em>myapp/forms.py</em></strong></p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">TicketForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">ModelForm</span><span class="p">):</span>
<span class="n">helper</span> <span class="o">=</span> <span class="n">FormHelper</span><span class="p">()</span>
<span class="n">helper</span><span class="o">.</span><span class="n">layout</span> <span class="o">=</span> <span class="n">Layout</span><span class="p">(</span>
<span class="n">Fieldset</span><span class="p">(</span>
<span class="c1"># ... all your layout stuff</span>
<span class="p">),</span>
<span class="n">FormActions</span><span class="p">(</span>
<span class="n">Submit</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span> <span class="s1">'Save'</span><span class="p">,</span> <span class="n">css_class</span><span class="o">=</span><span class="s2">"btn btn-outline-success"</span><span class="p">),</span>
<span class="n">HTML</span><span class="p">(</span><span class="s2">"""<a href="{</span><span class="si">% u</span><span class="s2">rl "ticket-list" %}" class="btn btn-secondary">Cancel</a>"""</span><span class="p">),</span>
<span class="n">HTML</span><span class="p">(</span><span class="s2">"""{</span><span class="si">% i</span><span class="s2">f object %}</span>
<span class="s2"> <a href="{</span><span class="si">% u</span><span class="s2">rl "ticket-delete" object.id %}"</span>
<span class="s2"> class="btn btn-outline-danger pull-right"></span>
<span class="s2"> Delete <i class="fa fa-trash-o" aria-hidden="true"></i></button></a></span>
<span class="s2"> {</span><span class="si">% e</span><span class="s2">ndif %}"""</span><span class="p">),</span>
<span class="p">)</span>
<span class="p">)</span>
</code></pre></div>
<p><strong><em>myapp/urls.py</em></strong></p>
<div class="highlight"><pre><span></span><code><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ticket/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">TicketListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ticket-list'</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ticket/new/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">TicketCreateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ticket-new'</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ticket/(?P<pk>[0-9]+)/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">TicketUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ticket-edit'</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ticket/(?P<pk>[0-9]+)/delete/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">TicketDeleteView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ticket-delete'</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<p><strong><em>myapp/views.py</em></strong></p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">TicketCreateView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Ticket</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">TicketForm</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">TicketUpdateView</span><span class="p">(</span><span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Ticket</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">TicketForm</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">TicketDeleteView</span><span class="p">(</span><span class="n">DeleteView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Ticket</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">TicketForm</span>
<span class="c1"># ...</span>
</code></pre></div>
<p><strong><em>templates/ticket/ticket_form.html</em></strong></p>
<div class="highlight"><pre><span></span><code>{% extends 'page_setup.html' %}
{% load crispy_forms_tags %}
{% block content %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"row"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"col"</span><span class="p">></span>
{% crispy form %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endblock %}
</code></pre></div>Remove unmanaged Nginx sites with Ansible2017-03-14T19:50:00-07:002017-03-14T19:50:00-07:00Justin Montgomerytag:justinmontgomery.com,2017-03-14:/remove-unmanaged-nginx-sites-with-ansible<p>Occasionally a yum update restores <code>conf.d/default.conf</code> on my CentOS 7 installs, and other times I just need to remove a site from its current server. My <a href="https://github.com/jmooo/ansible-nginx">Nginx role in Ansible</a> creates and updates server definitions for me, but I wanted the option to wipe out any configs I hadn't specifically defined for a server. It would take care of both my above cases, as well as any other site configs that may have snuck their way into my server, say if I had been testing something and left a config behind.</p>
<p>In the role <code>defaults/main.yml</code> I use a boolean that defaults to <code>no</code> for removing unmanaged sites. I like having to explicitly enable this behavior for each server since it is destructive.</p>
<p>In the first task I run a basic <code>find</code> command to locate all files regardless of extension in the Nginx config dir. I don't want anything but active configs in there. It is idempotent so allowed to run even in <code>--check</code> mode.</p>
<p>The second task required building the right <code>when:</code> filter, which was done with a little guidance from <a href="http://stackoverflow.com/questions/16385507/ansible-delete-unmanaged-files-from-directory">here</a> and <a href="https://gist.github.com/halberom/b1f6eaed16dba1b298e8">here</a>. My Nginx role mentioned above uses a <code>dict</code> with the base name of each config (ie: <code>myapp</code>) as the keys. We pass the keys into the Jinja2 filter that appends <code>.conf</code> to each key, then returns the modified keys as a list in the format: <code>[myapp.conf, othersite.conf, ...]</code>. With that list in hand it is easy to loop over the output of our <code>find</code> command and any filenames found which don't match our key list take a trip to our 51st <code>state: absent</code>. Get it? I'll see myself out.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># setting in role defaults</span>
<span class="nt">nginx_remove_unmanaged_sites</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="c1"># Find every file in the conf.d dir</span>
<span class="c1"># Allow to run in check mode, mark task as "never changed"</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Find existing site configs</span>
<span class="w"> </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">find /etc/nginx/conf.d -type f -printf "%f\n"</span>
<span class="w"> </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">contents</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">nginx_remove_unmanaged_sites</span>
<span class="w"> </span><span class="nt">check_mode</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="c1"># remove files found above that aren't in nginx_sites</span>
<span class="c1"># append '.conf' to each key in nginx_sites with some regex magic</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Remove unmanaged configs</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span>
<span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s">"/etc/nginx/conf.d/{{</span><span class="nv"> </span><span class="s">item</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">state</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">absent</span>
<span class="w"> </span><span class="nt">with_items</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">contents.stdout_lines</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">nginx_remove_unmanaged_sites and item not in nginx_sites.keys()|map('regex_replace','^(.*)$','\\1.conf')|list</span>
<span class="w"> </span><span class="nt">notify</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">reload nginx</span>
</code></pre></div>Speed up 'stat' command in Ansible2017-02-23T14:50:00-08:002017-02-23T14:50:00-08:00Justin Montgomerytag:justinmontgomery.com,2017-02-23:/speed-up-stat-command-in-ansible<p>My Ansible role to create a swapfile became painfully slow after its initial run. Turns out this was because once the swapfile is created Ansible's <a href="http://docs.ansible.com/ansible/stat_module.html"><code>stat</code></a> command takes a long time to calculate the checksum on a multi-gigabite file.</p>
<blockquote>
<p>$ time sha1sum swapfile</p>
<p>ff2975f9c13300a5b64c9d102fd2b83df4a1cd0f swapfile</p>
<p>real 2m21.507s</p>
</blockquote>
<p>Sweet lord two-and-a-half minutes ain't gonna fly. Since I simply want an existence check on a file all the "get" parameters of Ansible were superfluous, but they're all enabled by default.</p>
<p>The Ansible 2.2 way of speeding up a simple file existence check is:</p>
<div class="highlight"><pre><span></span><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Verify swapfile status</span>
<span class="w"> </span><span class="nt">stat</span><span class="p">:</span>
<span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">common_swapfile_location</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">get_checksum</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">get_md5</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">mime</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_status</span>
<span class="w"> </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">not swap_status.stat.exists</span>
</code></pre></div>
<p>The Ansible 2.3 way has a new check and a rename, so:</p>
<div class="highlight"><pre><span></span><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Verify swapfile status</span>
<span class="w"> </span><span class="nt">stat</span><span class="p">:</span>
<span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">common_swapfile_location</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">get_checksum</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">get_md5</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">get_mime</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">get_attributes</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_status</span>
<span class="w"> </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">not swap_status.stat.exists</span>
</code></pre></div>
<p>For posterity this is my <code>tasks/swapfile.yml</code>:</p>
<div class="highlight"><pre><span></span><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Verify swapfile status</span>
<span class="w"> </span><span class="nt">stat</span><span class="p">:</span>
<span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">common_swapfile_location</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">get_checksum</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">get_md5</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">mime</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">no</span>
<span class="w"> </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_status</span>
<span class="w"> </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">not swap_status.stat.exists</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Create swapfile</span>
<span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">dd if=/dev/zero of={{ common_swapfile_location }} bs=1M count={{ common_swapfile_size }}</span>
<span class="w"> </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_created</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_status.changed</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Set swapfile permissions</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span>
<span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">common_swapfile_location</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">root</span>
<span class="w"> </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">root</span>
<span class="w"> </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0600</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_status.stat.exists or swap_created.changed</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Format swapfile</span>
<span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">mkswap {{ common_swapfile_location }}</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_created.changed</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Enable swapfile</span>
<span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swapon {{ common_swapfile_location }}</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_created.changed</span>
<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Persist swapfile to fstab</span>
<span class="w"> </span><span class="nt">mount</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">none</span>
<span class="w"> </span><span class="nt">src</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">common_swapfile_location</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w"> </span><span class="nt">fstype</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap</span>
<span class="w"> </span><span class="nt">opts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">defaults</span>
<span class="w"> </span><span class="nt">passno</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0</span>
<span class="w"> </span><span class="nt">dump</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0</span>
<span class="w"> </span><span class="nt">state</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">present</span>
<span class="w"> </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">swap_created.changed</span>
</code></pre></div>Rotating logs with multiple workers in Django2017-01-24T15:56:00-08:002017-01-24T15:56:00-08:00Justin Montgomerytag:justinmontgomery.com,2017-01-24:/rotating-logs-with-multiple-workers-in-django<p>The <a href="https://docs.djangoproject.com/en/1.10/topics/logging/#examples">default</a> Django logging settings make use of <a href="https://docs.python.org/2/library/logging.handlers.html#filehandler"><code>FileHandler</code></a> which writes to a single file that grows indefinitely, or at least until your server vomits. You'll probably first reach for <a href="https://docs.python.org/2/library/logging.handlers.html#rotatingfilehandler"><code>RotatingFileHandler</code></a> or even better <a href="https://docs.python.org/2/library/logging.handlers.html#timedrotatingfilehandler"><code>TimedRotatingFileHandler</code></a> to solve your problem, but alas you're heading down a blind alley.</p>
<p>The problem, as myriad Stack Overflow <a href="http://stackoverflow.com/questions/18840785/timedrotatingfilehandler-doesnt-work-fine-in-django-with-multi-instance">questions</a> will <a href="http://stackoverflow.com/questions/32564472/multiple-processors-logging-to-same-rotate-file">tell you</a>, is if you are serving your app with something like gunicorn or uwsgi you're probably using multiple workers, which means multiple processes simultaneously trying to write and rotate logs. This leads to unexpected results such as; multiple log files changing at once, log files containing the wrong timestamped data, truncated logs and missing data. Ouch.</p>
<p>Since Django/Python can't be relied on to rotate logs in this scenario we turn to the trusty sysadmin's tonic: logrotate. However logrotate has a couple pitfalls of its own, such as using the <code>copytruncate</code> directive which can <em>also</em> lead to <a href="http://incoherency.co.uk/blog/stories/logrotate-copytruncate-race-condition.html">data loss</a>! So to avoid using that directive we'll settle on Python's <a href="https://docs.python.org/2/library/logging.handlers.html#watchedfilehandler"><code>WatchedFileHandler</code></a>, which detects file changes on disk and can continue logging appropriately, whereas <code>FileHandler</code> would either continue writing to the old log file or just stop writing logs entirely.</p>
<p>In the end your <code>settings/base.py</code> logging setup should look something like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">LOGGING</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'version'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s1">'disable_existing_loggers'</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="s1">'formatters'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'verbose'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'format'</span><span class="p">:</span> <span class="s2">"[</span><span class="si">%(asctime)s</span><span class="s2">] </span><span class="si">%(levelname)s</span><span class="s2"> [line:</span><span class="si">%(lineno)s</span><span class="s2">] </span><span class="si">%(message)s</span><span class="s2">"</span><span class="p">,</span>
<span class="s1">'datefmt'</span><span class="p">:</span> <span class="s2">"</span><span class="si">%d</span><span class="s2">/%b/%Y %H:%M:%S"</span>
<span class="p">},</span>
<span class="s1">'simple'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'format'</span><span class="p">:</span> <span class="s1">'</span><span class="si">%(levelname)s</span><span class="s1"> </span><span class="si">%(message)s</span><span class="s1">'</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'file'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'logging.handlers.WatchedFileHandler'</span><span class="p">,</span>
<span class="s1">'filename'</span><span class="p">:</span> <span class="s1">'/var/log/myapp/myapp.log'</span><span class="p">,</span>
<span class="s1">'formatter'</span><span class="p">:</span> <span class="s1">'verbose'</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'loggers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'django'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'file'</span><span class="p">],</span>
<span class="s1">'propagate'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Then I created a basic logrotate config but doing a dry-run test reported this error:</p>
<blockquote>
<p>$ logrotate -d /etc/logrotate.d/myapp</p>
<p>error: skipping "/var/log/myapp/myapp.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.</p>
</blockquote>
<p>Turns out that error is because /var/log/myapp is owned by the gunicorn user (which serves the django app, and thus writes the logs, and thus owns the directory). The <code>su</code> directive lets you set the owner and group logrotate should run as to solve that problem.</p>
<p>I also used the <code>dateyesterday</code> directive to backdate the rotated log files. Otherwise since <a href="https://www.cyberciti.biz/faq/linux-when-does-cron-daily-weekly-monthly-run/">anacron</a> runs at 3am (the default on RHEL/CentOS) the filename wouldn't match the timestamps inside.</p>
<p>My final logrotate config looks like:</p>
<div class="highlight"><pre><span></span><code><span class="o">/</span><span class="k">var</span><span class="o">/</span><span class="nb">log</span><span class="o">/</span><span class="n">myapp</span><span class="o">/</span><span class="n">myapp</span><span class="o">.</span><span class="n">log</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">daily</span>
<span class="w"> </span><span class="n">rotate</span><span class="w"> </span><span class="mi">30</span>
<span class="w"> </span><span class="n">create</span>
<span class="w"> </span><span class="n">dateext</span>
<span class="w"> </span><span class="n">dateyesterday</span>
<span class="w"> </span><span class="n">compress</span>
<span class="w"> </span><span class="n">delaycompress</span>
<span class="w"> </span><span class="n">notifempty</span>
<span class="w"> </span><span class="n">missingok</span>
<span class="w"> </span><span class="n">su</span><span class="w"> </span><span class="n">gunicorn</span><span class="w"> </span><span class="n">web</span>
<span class="p">}</span>
</code></pre></div>
<p>If you're really set on letting Python handle log rotation you can look into the <a href="https://pypi.python.org/pypi/ConcurrentLogHandler/0.9.1"><code>ConcurrentLogHandler</code></a> package; however it only rotates based on size, not date.</p>Measure IFTTT traffic via naked 'curl' user agent string2016-10-11T22:13:00-07:002016-10-11T22:13:00-07:00Justin Montgomerytag:justinmontgomery.com,2016-10-11:/measure-ifttt-traffic-via-naked-curl-user-agent-string<p>I was curious if I could measure <a href="https://ifttt.com">IFTTT</a> traffic to my site so I setup a simple RSS-to-email recipe while tailing my access logs. Turns out their user agent string only identifies itself as 'curl':</p>
<div class="highlight"><pre><span></span><code><span class="mf">54.172.140.57</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">00</span><span class="p">:</span><span class="mf">59</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">52.91.39.135</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">06</span><span class="p">:</span><span class="mf">09</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.162.188.32</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">11</span><span class="p">:</span><span class="mf">15</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.204.197.181</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">16</span><span class="p">:</span><span class="mf">16</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">107.22.155.34</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">21</span><span class="p">:</span><span class="mf">21</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.84.202.24</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">26</span><span class="p">:</span><span class="mf">47</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.165.18.27</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">32</span><span class="p">:</span><span class="mf">33</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.237.233.244</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">40</span><span class="p">:</span><span class="mf">11</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.211.88.14</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">46</span><span class="p">:</span><span class="mf">07</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.235.54.123</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">52</span><span class="p">:</span><span class="mf">15</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">54.198.2.88</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">22</span><span class="p">:</span><span class="mf">58</span><span class="p">:</span><span class="mf">35</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
<span class="mf">184.72.126.209</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="err">[</span><span class="mf">11</span><span class="o">/</span><span class="n">Oct</span><span class="o">/</span><span class="mf">2016</span><span class="p">:</span><span class="mf">23</span><span class="p">:</span><span class="mf">06</span><span class="p">:</span><span class="mf">11</span><span class="w"> </span><span class="o">-</span><span class="mf">0700</span><span class="err">]</span><span class="w"> </span><span class="s">"GET /feeds/atom.xml HTTP/1.1"</span><span class="w"> </span><span class="mf">200</span><span class="w"> </span><span class="mf">13473</span><span class="w"> </span><span class="s">"-"</span><span class="w"> </span><span class="s">"curl"</span><span class="w"> </span><span class="s">"-"</span>
</code></pre></div>
<p>Additionally IFTTT appears to check the site for updates every 5 minutes, and all the IPs I ran though <a href="https://www.iplocation.net/">IP geolocation</a> were from the us-east-1 AWS region. <a href="https://twitter.com/miyagawa/status/682239647963938816">This tweet</a> suggests the same, and that there are a lot more IPs than I saw. Unfortunately their engineering blog isn't very active anymore but they do have a <a href="http://engineering.ifttt.com/data/2015/10/14/data-infrastructure/">fantastic post</a> about their infrastructure.</p>
<p>Running curl from macOS and CentOS included a version number in the user agent:</p>
<div class="highlight"><pre><span></span><code>[11/Oct/2016:22:08:15 -0700] "GET / HTTP/1.1" 200 9458 "-" "curl/7.49.1" "-"
[11/Oct/2016:22:08:36 -0700] "GET / HTTP/1.1" 200 9458 "-" "curl/7.29.0" "-"
</code></pre></div>
<p>In fact running <code>zgrep "\"curl\"" *</code> to scan my gzipped access logs for that naked curl UA among all the sites I administer only turned up IFTTT hits. Maybe behind the scenes IFTTT really is just making curl calls, and hiding the version token is simply to deny a piece of information about their systems to potential attackers? A more cynical mind might think they were trying to hide their traffic so it would be harder to identify or block.</p>
<p>Whatever the case, monitoring for a version stripped 'curl' UA appears to be the best method to guesstimate your IFTTT traffic currently, especially since the requests themselves come from many different IPs which could easily change over time.</p>Install mysqlclient for Django 1.10 on macOS2016-10-02T00:14:00-07:002016-10-02T00:14:00-07:00Justin Montgomerytag:justinmontgomery.com,2016-10-02:/install-mysqlclient-for-django-110-on-macos<p>I was trying to get a fresh Django 1.10 project setup on macOS Sierra using MySQL and Python 3.5 in a venv but <code>pip install mysqlclient</code> was failing with the error:</p>
<div class="highlight"><pre><span></span><code>ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: command 'clang' failed with exit status 1
</code></pre></div>
<p>As is often the case after some searching I came into <a href="http://stackoverflow.com/a/39244687/1107232">the solution</a> on Stack Overflow. <code>mysqlclient</code> needs to link against the homebrew version of openssl I have installed:</p>
<div class="highlight"><pre><span></span><code>env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install mysqlclient
</code></pre></div>
<p>The solution post also mentions that installing the xcode command line tools via:</p>
<div class="highlight"><pre><span></span><code>xcode-select --install
</code></pre></div>
<p>will provide the required ssl libs for the <strong><em>system</em></strong> installed version of python (2.7 on Sierra), it will not work for python in a virtual environment.</p>Favorite Historical Documentaries2016-05-06T14:33:00-07:002016-12-07T14:33:00-08:00Justin Montgomerytag:justinmontgomery.com,2016-05-06:/favorite-historical-documentaries<p>I watch a lot of history documentaries, because why wouldn't you? The older the subject the better. I've spent enough time searching for lists of other peoples favorite docs and for <em>"if you liked that you'll love this"</em> recommendations that I figured I should post my own favorites. Hopefully you find a doc in here you haven't seen, and I'd love any recommendations in the comments!</p>
<ul>
<li>
<h3><a href="https://en.wikipedia.org/wiki/The_Dark_Ages:_An_Age_of_Light">The Dark Ages: An Age of Light</a> – 2012, 4 episodes, BBC</h3>
<p><a href="https://en.wikipedia.org/wiki/Waldemar_Januszczak">Waldemar Januszczak</a> does a great job further debunking the view that the Dark Ages was an era of civilization wandering the wilderness; that after the Roman Empire fell nothing worthwhile happened until the Italian Renaissance.</p>
<p>Over the four episodes he visits many cultures from this time period; the Romans, Huns, Vandals, Visigoths, Moors, Arabs, Carolingians, Vikings and Anglo-Saxons. I especially enjoyed episode 3 because of course the "Dark Ages" in Europe was right during the <a href="https://en.wikipedia.org/wiki/Islamic_Golden_Age">Islamic Golden Age</a>, and episode 4 which spent quite a bit of time on the exquisite jewelry crafting of the Anglo-Saxons exemplified by the finds at <a href="https://en.wikipedia.org/wiki/Sutton_Hoo">Sutton Hoo</a> and <a href="https://en.wikipedia.org/wiki/Staffordshire_Hoard">Staffordshire</a>.</p>
<p>Waldemar is fantastic at highlighting the details in the period's art and architecture, more so than any other similar docs I've seen. He explains that the iconic double arches of the <a href="https://en.wikipedia.org/wiki/Mosque%E2%80%93Cathedral_of_C%C3%B3rdoba">Mosque of Córdoba</a> are because columns are a real pain to make, and you'll avoid doing it if you can. So the columns were stolen from other buildings, but they were too short to create the open and airy space the Moors were after. So the solution was to add a second arch on top of the first to really open up the room. He also explains mosques are modeled after the home of Muhammad himself. Exemplified by the <a href="https://en.wikipedia.org/wiki/Mosque_of_Ibn_Tulun">Mosque of Ibn Tulun</a> the large open courtyard allowed room for followers, with surrounding shaded arcades providing relief from the sun and heat.</p>
<p>I'm going to have to search for more docs by Waldemar because I really like his energy and obvious passion in his work, it's infectious.</p>
<p>(watch: <a href="https://www.amazon.com/Dark-Ages-Age-Light/dp/B00J5R20MA">Amazon Video</a>)</p>
</li>
<li>
<h3><a href="http://www.starmediafilm.com/en/cinema_hall/romanovi/trejler">The Romanovs</a> – 2014, 8 episodes, Channel One</h3>
<p>History of the <a href="https://en.wikipedia.org/wiki/House_of_Romanov">House of Romanov</a> beginning after the <a href="https://en.wikipedia.org/wiki/Time_of_Troubles">Time of Troubles</a> with the 1613 coronation of <a href="https://en.wikipedia.org/wiki/Michael_I_of_Russia">Michael I</a> and ending with the 1917 <a href="https://en.wikipedia.org/wiki/Execution_of_the_Romanov_family">execution of Nicholas II</a> and his family by Bolsheviks in an Yekaterinburg basement. Russian produced documentary but with English dubbing, all the screen text is still in Russian but you don't miss anything and honestly gives the doc a great atmosphere for non-Russian speakers.</p>
<p>Also it made me realize I need to cast my net wider because there are some great documentaries produced outside of America and Britain. <a href="https://www.youtube.com/channel/UCuSx-lf2ft7hPceGVNHybOw">Star Media</a> has a ton of content with at least English subtitles and a few with full English dubbing.</p>
<p>(watch: <a href="https://www.youtube.com/playlist?list=PLwGzY25TNHPBfaoOR3pXw3VyBvmXljeio">YouTube</a>)</p>
</li>
<li>
<h3><a href="https://en.wikipedia.org/wiki/The_Great_War_%28documentary%29">The Great War</a> – 1964, 26 episodes, BBC</h3>
<p>Maybe my favorite documentary ever, the kind you couldn't make anymore because of all the first person interviews. Made on the 50th anniversary of the outbreak of WWI it's amazing to see the soldiers and people who lived through it in their 60s and 70s, especially since the last WWI veteran <a href="https://en.wikipedia.org/wiki/List_of_last_surviving_World_War_I_veterans_by_country">died in 2012</a>.</p>
<p>Excellent exposition on the pre-war political, economic and social situation of the belligerents. Understanding what Germany, Russia, etc were like before 1914 goes a long way to explaining how they each prosecuted the war. </p>
<p>(watch: <a href="https://www.youtube.com/playlist?list=PLdEBPyoq11-7H07u7iwGM_3l-_QfxFj9B">YouTube</a>) </p>
</li>
<li>
<h3><a href="https://en.wikipedia.org/wiki/The_World_at_War">The World at War</a> – 1973, 26 episodes, ITV</h3>
<p>The WWII equivalent of The Great War. Consistently referenced as the best documentary ever, it doesn't disappoint. The length of the series really lets them expound on specific theaters such as the U-boat wolfpacks, including commentary with Karl Dönitz himself. Maybe that's the most surreal part of this series to me, interviews from the early '70s with names synonymous with WWII: Albert Speer, Curtis LeMay, Traudl Junge, Paul Tibbets, Alger Hiss, "Bomber" Harris and on and on.</p>
<p>One particularly poignant moment was an interview with a member of the Japanese envoy signing their surrender aboard the USS Missouri, who recounted his thoughts and described the scene:</p>
<blockquote>
<p>I saw many thousands of sailors everywhere on this huge vessel. And just in front of us we had delegates of the victorious powers in military uniforms, glittering with gold. And looking at them I wondered how Japan ever thought she could defeat all those nations. — <cite><a href="https://en.wikipedia.org/wiki/Toshikazu_Kase">Toshikazu Kase</a></cite></p>
</blockquote>
<p>(watch: <a href="http://www.amazon.com/A-New-Germany-1933-1939/dp/B016X38766/">Amazon Prime</a>)</p>
</li>
<li>
<h3><a href="https://en.wikipedia.org/wiki/World_War_II_in_HD_Colour">World War II in HD Colour</a> – 2008, 13 episodes, Military Channel</h3>
<p>A more modern and shorter WWII documentary (which of course does not mean better) than The World at War. The original and colorized footage looks perfectly natural and adds great depth to the series. Like most of my favorite documentaries it spends ample time on the pre-war situation in Germany, Japan, Russia and other countries.</p>
<p>(watch: <a href="https://www.netflix.com/title/70254851">Netflix</a>)</p>
</li>
</ul>cd shortcuts with CDPATH and bash tab completion2016-04-19T23:08:00-07:002016-04-19T23:08:00-07:00Justin Montgomerytag:justinmontgomery.com,2016-04-19:/cd-shortcuts-with-cdpath-and-bash-tab-completion<p>For years I've had some fairly hacky ways of changing directories quicker. I'd wager a nickel these kind of aliases are pretty common:</p>
<div class="highlight"><pre><span></span><code><span class="nb">alias</span><span class="w"> </span><span class="nv">cdblog</span><span class="o">=</span><span class="s1">'cd ~/sites/my_blog'</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">cdapp</span><span class="o">=</span><span class="s1">'cd ~/repos/my_app'</span>
</code></pre></div>
<p>That grows pretty unwieldy when working with a bunch of repos. So then I thought I was super clever by using bash functions to cd directly into a git repo, since all my repos were in one of two parent dirs:</p>
<div class="highlight"><pre><span></span><code><span class="k">function</span><span class="w"> </span>repos<span class="o">(){</span><span class="w"> </span><span class="nb">cd</span><span class="w"> </span>~/repos/<span class="nv">$1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
<span class="k">function</span><span class="w"> </span>sites<span class="o">(){</span><span class="w"> </span><span class="nb">cd</span><span class="w"> </span>~/sites/<span class="nv">$1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
me@local$<span class="w"> </span>repo<span class="w"> </span>my_app
me@local$<span class="w"> </span>sites<span class="w"> </span>my_blog
</code></pre></div>
<p>It was a bit more typing, but fewer aliases to remember and more scalable. However the loss of tab completion using this method is a bummer.</p>
<p>When I finally got annoyed enough to look for a better way, of course it was waiting for me in a <a href="http://superuser.com/questions/216803/bash-is-there-a-way-to-use-tab-completion-through-out-cdpath">superuser post</a>. Using the <a href="http://caliban.org/bash/">CDPATH</a> environment variable and the <code>bash-completion</code> package (same name on centos/fedora/os x/ubuntu) I can now cd into my most used directories, complete with tab completion, from anywhere on the cli.</p>
<ol>
<li>
<p>Install bash-completion, substituting appropriate package manager:</p>
<div class="highlight"><pre><span></span><code>brew<span class="w"> </span>install<span class="w"> </span>bash-completion
</code></pre></div>
</li>
<li>
<p><strong>OS X ONLY</strong>: append to your ~/.bash_profile:</p>
<div class="highlight"><pre><span></span><code><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-f<span class="w"> </span><span class="k">$(</span>brew<span class="w"> </span>--prefix<span class="k">)</span>/etc/bash_completion<span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span>.<span class="w"> </span><span class="k">$(</span>brew<span class="w"> </span>--prefix<span class="k">)</span>/etc/bash_completion
<span class="k">fi</span>
</code></pre></div>
</li>
<li>
<p>Everyone append to your ~/.bash_profile:</p>
<div class="highlight"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">CDPATH</span><span class="o">=</span>.:~:~/repos:~/sites
</code></pre></div>
</li>
<li>
<p>Activate changes:</p>
<div class="highlight"><pre><span></span><code><span class="nb">source</span><span class="w"> </span>~/.bash_profile
</code></pre></div>
</li>
<li>
<p>Kick the tires, from say /tmp:</p>
<div class="highlight"><pre><span></span><code>me@local<span class="w"> </span>tmp$<span class="w"> </span><span class="nb">cd</span><span class="w"> </span>my<TAB><TAB>
my_blog/<span class="w"> </span>my_app/
</code></pre></div>
</li>
</ol>
<p>Tab completion should be working as you'd expect against all your favorite directories, no matter where you are on the cli. This made navigating around dozens of repos quite a bit easier for me, though for a handful of my most used ones I may still treat myself to a dedicated alias. Because I'm weak.</p>Tomato Advanced Firewall Settings2011-07-29T13:58:00-07:002011-07-29T13:58:00-07:00Justin Montgomerytag:justinmontgomery.com,2011-07-29:/tomato-advanced-firewall-settings<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>A user commented on the <a href="https://justinmontgomery.com/wake-on-lan-configuring-your-pc-tomato-iphone">Tomato Wake-on-LAN</a> post:</p>
<blockquote>
<p>I found I couldn’t get wake on lan to work at all until I enabled Advanced->Firewall->Allow multicast.</p>
</blockquote>
<p>Well that made me wonder what all those advanced settings did, and turns out the descriptions available suck! Well sometimes there wasn’t even a description to label as “suck” so lets put some descriptions in Google that are at least marginally better. In italics is the setting explanation from the <a href="https://en.wikibooks.org/wiki/Tomato_Firmware/Menu_Reference#Firewall">Tomato manual</a> at Wikibooks:</p>
<p><img alt="Tomato advaned firewall settings" src="https://justinmontgomery.com/images/tomato-advanced-firewall-settings.png"></p>
<p><strong>Respond to ICMP ping</strong> – <em>If checked the router will respond to ping requests from on the WAN interface. (Default: unchecked)</em></p>
<p>If you plan on using Wake-on-LAN this must be checked or the router will ignore the Magic Packet that actually tells your computer to turn on. If you don’t need to access your network remotely you can leave it unchecked.</p>
<p><strong>Allow multicast</strong> – <em>If checked, the router will allow multicast packets to reach the LAN. (Default: unchecked)</em></p>
<p><a href="http://en.wikipedia.org/wiki/Multicast">Multicast</a> is a “one-to-many” communication method so a computer can send data to several computers at once via a single packet, improving efficiency. It is frequently used for streaming video and you may notice performance gains/losses by enabling/disabling it, total crapshoot. You can leave this unchecked most likely. Just to be confusing some routers like Linksys call this option “filter multicast” in which case you would leave that setting checked to disable (i.e. filter out) the multicast packets.</p>
<p><strong>NAT loopback</strong> – <em>If checked, the router allows LAN devices to reach other LAN devices via the router’s WAN IP address and a properly configured port forward. If unchecked, LAN devices can only contact other LAN devices via their local IP addresses. (Default: Forwarded only)</em></p>
<p>This one is a little complicated but DynDNS has a <a href="http://www.dyndns.com/support/kb/loopback_connections.html">good description</a> with a diagram. For example the loopback problem occurs when there is a webserver on the same subnet with you. If you try to visit that webserver by browsing to TheWebserver.com or whatever it’s domain is the router would try to send you out onto the internet to visit the site. Problem is the server isn’t out on the internet from your perspective, it’s on your local area network. Most users will want to leave this at the default setting of Forwarded only. If you want more crazy detail on what is happening <a href="http://www.dslreports.com/forum/r22683726-Tomato-tomato-firmware-NAT-loopback-settings">this thread</a> should prove helpful.</p>
<p><strong>Enable SYN cookies</strong> – <em>Activates SYN cookies. (Default: unchecked)</em></p>
<p>Probably the best description in the whole book! SYN cookies are a tool for thwarting a <a href="http://en.wikipedia.org/wiki/SYN_flood">SYN flood</a>, an older type of DoS attack. I would enable this unless you find it causing problems with your router. The Tomato developer has <a href="http://www.linksysinfo.org/index.php?threads/syn-cookies.28220/#post-135367">commented</a> about sparse and unconfirmed reports of issues with the setting.
Well that’s something at least, hopefully it’s all correct yeah?!</p>Wake-on-LAN: Configuring your PC, Tomato & iPhone2011-04-01T14:09:00-07:002011-04-01T14:09:00-07:00Justin Montgomerytag:justinmontgomery.com,2011-04-01:/wake-on-lan-configuring-your-pc-tomato-iphone<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>So <a href="http://lifehacker.com/5786791/rule-your-computer-from-afar-by-setting-up-wake-on-lan">this article at Lifehacker</a> got me thinking about setting up Wake-on-LAN (WoL) for my system. For several years I just left my PC on 24/7 to VNC into it anytime, which I stopped doing because it was a massive waste of electricity (i.e. money dollars). I don’t need into my computer remotely very often but WoL turned out to be the optimal solution for when I do. My personal setup required several things:</p>
<h3>Enable WoL in the computer’s BIOS</h3>
<ul>
<li>Configure Linksys router running <a href="http://tomatousb.org/">Tomato USB</a> firmware</li>
<li>Setup DynDNS to always be able to locate my network from the internet</li>
<li>Find an iPhone app to send the “magic packet“, which tells your computer to turn on</li>
<li>Enabling WoL in your BIOS</li>
</ul>
<p>Both my PCs are somewhat older and don’t have settings labeled “Wake-on-LAN” but instead “Wake-on-PME” (power management event) which is the same thing, enable the option and boot your computer.</p>
<p>On Windows 7 both my machines had WoL enabled by default but to verify you can open your network adapter in Device Manager and check the Advanced tab to see if the property pertaining to WoL is enabled. For me one machine used the property “Wake on <a href="https://en.wikipedia.org/wiki/Wake-on-LAN#Magic_packet">Magic Packet</a>” and the value “Enabled”, another used the property “Wake-On-LAN Capabilities” and the value “Pattern Match & Magic Packet”. It’s all driver specific so yours could be a variation of those.</p>
<p>Ethernet adapter in Device Manager:</p>
<p><img alt="Wake-on-LAN Device Manager" src="https://justinmontgomery.com/images/wake-on-lan-device-manager.png"></p>
<p>WoL doesn’t always work when the computer is off (depends on the machine), because the network card has no power and thus can’t receive the magic packet, so your PC may have to be in Sleep or Hibernate mode. In Win7 you can change the default shutdown option to hibernate; click the Start orb and right click on Shutdown, choose properties, change the Power button action drop-down to Hibernate and click OK.</p>
<h3>Configuring Tomato for WoL</h3>
<p>After you have <a href="http://www.polarcloud.com/tomato">Tomato</a> (or <a href="http://tomatousb.org/">Tomato USB</a>) installed take a few minutes to <a href="https://justinmontgomery.com/securely-access-your-tomato-router-remotely">secure it for remote access</a>. This will let you log into the web interface or SSH in to a command line for testing or other uses in the future.</p>
<p>Tomato has built in WoL functions under Tools->WOL where you can click any MAC address listed to send the wakeup command. For easier identification I’ve given all my devices static IPs and names under Basic->Static DHCP. Worst case you can always log into your router this way to boot your computer, bypassing the complexity and problems of WAN control as Tomato effectively sends the command from inside your LAN.</p>
<p>The hiccup for internet control of WoL is when your computer has been off for 20 minutes or so you won’t be able to send the magic packet from the WAN anymore. You can still login to Tomato and wakeup the computer but that’s it. This is because it’s address has expired from the <a href="https://en.wikipedia.org/wiki/Address_Resolution_Protocol">ARP cache</a> and you’re boned. The Tomato WoL page lists device status, if it is “Active (In ARP)” then internet WoL commands will work, otherwise no go.</p>
<p><a href="http://www.wolcenter.com/faq.php">The solution</a> is to send the magic packet to the broadcast address for your subnet (generally 192.168.1.255) but Tomato won’t allow that, so we work around it by turning 192.168.1.254 into the broadcast address by adding these two lines to Administration->Scripts->Firewall:</p>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal">1</span>
<span class="normal">2</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nx">ip</span><span class="w"> </span><span class="nx">neigh</span><span class="w"> </span><span class="nx">change</span><span class="w"> </span><span class="m m-Double">192.168.1.254</span><span class="w"> </span><span class="nx">lladdr</span><span class="w"> </span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="w"> </span><span class="nx">nud</span><span class="w"> </span><span class="nx">permanent</span><span class="w"> </span><span class="nx">dev</span><span class="w"> </span><span class="nx">br0</span>
<span class="nx">ip</span><span class="w"> </span><span class="nx">neigh</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="m m-Double">192.168.1.254</span><span class="w"> </span><span class="nx">lladdr</span><span class="w"> </span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="w"> </span><span class="nx">nud</span><span class="w"> </span><span class="nx">permanent</span><span class="w"> </span><span class="nx">dev</span><span class="w"> </span><span class="nx">br0</span>
</code></pre></div></td></tr></table></div>
<p>How Tomato looks:</p>
<p><img alt="Tomato firewall settings" src="https://justinmontgomery.com/images/wake-on-lan-tomato-firewall.png"></p>
<p>Reboot your router. If you secured your router for remote access as suggested earlier, SSH into it and run the following command to see if the output includes your new settings:</p>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal">1</span>
<span class="normal">2</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nx">user</span><span class="err">@</span><span class="nx">server</span><span class="err">$</span><span class="w"> </span><span class="nx">ip</span><span class="w"> </span><span class="nx">neigh</span><span class="w"> </span><span class="nx">show</span>
<span class="m m-Double">192.168.1.254</span><span class="w"> </span><span class="nx">dev</span><span class="w"> </span><span class="nx">br0</span><span class="w"> </span><span class="nx">lladdr</span><span class="w"> </span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="p">:</span><span class="nx">ff</span><span class="w"> </span><span class="nx">PERMANENT</span>
</code></pre></div></td></tr></table></div>
<p>You’re in business, now go to Port Forwarding and setup a forward for UDP on some high port (5500, 8888, whatever) to internal address 192.168.1.254. Now internet originated magic packets will be broadcast to all devices on your network, but only the machines with the correct MAC address within that packet will respond.</p>
<p>This workaround of the ARP expiration problem does introduce a minor security issue that most home users can safely ignore. We’ve created what is called a <a href="https://en.wikipedia.org/wiki/Wake-on-LAN#Subnet_directed_broadcasts">Subnet Directed Broadcast</a> which if an attacker knows your WAN IP, the port number you forwarded, and that there is a SDB on that port they could initiate a type of DDoS called a <a href="https://en.wikipedia.org/wiki/Smurf_attack">Smurf attack</a>. In reality you could be just as easily DDoS’ed without SDB set up and besides, the bad people are more concerned with infecting your PC to steal data, sell you fake anti-virus software or act as a zombie in some botnet, not blocking you from playing World of Warcraft.</p>
<h3>Setup DynDNS to locate your network from the internet</h3>
<p>While you could just use your WAN IP address directly, available from the Tomato status page or <a href="http://icanhazip.com/">icanhazip.com</a>, the pro route is to setup a DDNS (Dynamic DNS) service like <a href="http://dyn.com/dns/?rdr=dyndnsorg">DynDNS</a> as <a href="http://lifehacker.com/124804/geek-to-live--how-to-assign-a-domain-name-to-your-home-web-server">detailed here</a>. That way you get an easy to remember subdomain like SweetMeats.DynDNS.org and when your ISP inevitably changes your IP address Tomato will notify DynDNS and you won’t notice anything has happened.</p>
<p>At this point you should be able to boot your computer via the internet, hibernate your computer and use this handy website to <a href="https://www.depicus.com/wake-on-lan/woli">send a magic packet</a>. If my instructions are worth their salt you should hear your loud-ass computer booting up, WHIRRR.</p>
<h3>Sending the Magic Packet from your iOS Device</h3>
<p>I tried a few WoL iPhone apps before settling on <a href="https://itunes.apple.com/us/app/remoteboot-lite/id310369182?mt=8">RemoteBoot Lite</a> which is free and works perfectly. For the most part paid WoL apps in the AppStore are ripoffs, why pay $3+ for a function as basic as sending the magic packet? Anyways, type in your DynDNS domain under Name, the MAC address to your computer (from Tomato: Status->Device List or via <a href="http://www.wikihow.com/Find-the-MAC-Address-of-Your-Computer">other methods</a>), tap the DNS button which will resolve your domain to an IP address and set Target Port to whichever port you setup for UDP forwarding earlier.</p>
<p><img alt="RemoteBoot Lite" src="https://justinmontgomery.com/images/remote-boot-lite.png"></p>
<p>That’s it, tap the Boot button and if your computer is hibernating or asleep it should wake up. It took a lot of trial and error to get to this point however and I did a lot of backtracking so don’t give up if it doesn’t work on your first attempt, but if I was to do it all over again this would be the shortest path.</p>
<p>Well this ended up being a lot more in-depth than originally intended. Hopefully someone can use it.</p>Securely Access Your Tomato Router Remotely2011-04-01T00:51:00-07:002011-04-01T00:51:00-07:00Justin Montgomerytag:justinmontgomery.com,2011-04-01:/securely-access-your-tomato-router-remotely<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>While I was configuring <a href="https://justinmontgomery.com/wake-on-lan-configuring-your-pc-tomato-iphone">Wake-on-LAN</a> for my computers I took some time to enable remote access to my <a href="http://tomatousb.org/">Tomato</a> router and secure it properly, <a href="http://www.overclock.net/networking-security/489538-tutorial-secure-your-network-tomato.html">this forum post</a> was especially helpful. Under Administration->Admin Access the most important settings are:</p>
<ul>
<li>
<p>Under <strong>Local Access</strong> choose HTTP & HTTPS or just HTTPS, the default ports of 80 and 443 respectively are fine, as this is behind the firewall that is your router.</p>
</li>
<li>
<p>Under <strong>Remote Access</strong> select HTTPS and choose an unused port, something like 2525 or 8998.</p>
</li>
<li>
<p>For the SSH Daemon check <strong>Enable at Startup</strong>, <strong>Remote Access</strong>, choose an unused port for <strong>Remote Port</strong> as above, check <strong>Remote Forwarding</strong> and leave at the default port of 22 and check <strong>Allow Password Login</strong>.</p>
</li>
<li>
<p>Disable <strong>Telnet Daemon</strong>, it’s too insecure to ever consider using. Also you can <strong>Limit Connection Attempts</strong> to some low-ish number for bonus security.</p>
</li>
<li>
<p>Finally choose <strong>Password</strong> that isn’t embarrassingly easy to crack, such as happened with <a href="http://arstechnica.com/tech-policy/news/2011/02/anonymous-speaks-the-inside-story-of-the-hbgary-hack.ars/">Anonymous vs HBGary</a> and <a href="http://www.lightbluetouchpaper.org/2011/02/09/measuring-password-re-use-empirically/">Gawker accounts</a> recently.</p>
</li>
</ul>
<p>Instead of Allow Password Login some people prefer use the Authorized Keys option, which is fine as it is generally more secure than a password, but also more of a hassle in a home network setting. Since you can already log into the router’s interface with a password what is the big deal about using SSH with a password? Nothing that’s what. If you’d like to go the Key route <a href="http://siteadmin.gforge.inria.fr/ssh_windows.html">this is a good walkthrough</a> using <a href="http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html">PuTTYgen</a>.</p>
<p>Speaking of PuTTY you should go download it, as it is the best SSH client available for windows and it’s free. If you’re not familiar with PuTTY this is a <a href="http://www.electrictoolbox.com/article/applications/ssh-putty/">handy guide</a>. Now you should be able to connect to your router remotely though your WAN IP (easier if you’ve <a href="http://lifehacker.com/124804/geek-to-live--how-to-assign-a-domain-name-to-your-home-web-server">setup a DynDNS account</a>) and the Remote Access port you selected above. In a pinch you can also use SSH from your iPhone but from <a href="http://stdout-dev-null.blogspot.com/2010/05/free-ssh-client-for-iphone-ipod-touch.html">what I’ve read</a> the only free SSH app is the <a href="http://itunes.apple.com/us/app/rove-mobile-admin-client/id339820302?mt=8">Rove Mobile Admin</a> Client, of course there are plenty of paid ones such as <a href="http://itunes.apple.com/us/app/issh-ssh-vnc-console/id287765826?mt=8">iSSH</a> if you’ve got $10 to blow.</p>
<p><img alt="Tomato admin setup" src="https://justinmontgomery.com/images/tomato-admin-setup.png"></p>Sync and Secure FileZilla with Dropbox2011-03-06T19:38:00-08:002011-03-06T19:38:00-08:00Justin Montgomerytag:justinmontgomery.com,2011-03-06:/sync-and-secure-filezilla-with-dropbox<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>Searching for more ideas of how to synchronize my computers with <a href="http://db.tt/ig4CwZd">Dropbox</a> and secure them with <a href="http://www.keepass.info/">KeePass</a> led me to a way to sync both the settings and FTP site list for <a href="http://filezilla-project.org/">FileZilla</a>. FileZilla is of course the best FTP client available so if you aren’t currently using it, start. It does have one horrible problem though, it stores your passwords as plaintext! That is easily fixed though, onward:</p>
<p><img alt="FileZilla config file" src="https://justinmontgomery.com/images/filezilla-config.png"></p>
<p>Building off <a href="http://www.scottsawyerconsulting.com/content/keeping-filezilla-site-manager-synced-multiple-computers">this guide</a> the method is:</p>
<ol>
<li>
<p>Create a directory called “FileZilla” in your Dropbox folder</p>
</li>
<li>
<p>Open FileZilla, File->Export check “export site manager entires” and save it in your new FileZilla folder in Dropbox as “sitemanager.xml” and close FileZilla</p>
</li>
<li>
<p>Go to the install location of FileZilla, probably C:\Program Files (x86)\FileZilla FTP Client\Docs</p>
</li>
<li>
<p>Open the “fzdefaults.xml.example” in a text editor</p>
</li>
<li>
<p>Scroll down to the area you see in the picture at right, delete everything between and including <Servers></Servers>. That is an example site which you don’t need, and your sites are in sitemanager.xml anyways.</p>
</li>
<li>
<p>In the “Config location” setting change $SOMEDIR/filezilla/ to the path to your Dropbox filezilla folder, probably C:\Users\<User>\Dropbox\FileZilla This is the folder FileZilla will start storing your filezilla.xml (settings) and sitemanager.xml (ftp site list) files, among other config files.</p>
</li>
<li>
<p>BONUS: For extra security change “Kiosk mode” to 1, this will stop FileZilla from remembering passwords which are embarrassingly stored as plaintext in the sitemanager.xml file!! Use KeePass to manage your FTP passwords instead, less convenient but infinitely more secure. (This <a href="http://forum.filezilla-project.org/viewtopic.php?p=30742#p30742">forum post</a> gives an explanation of why the passwords are plaintext)</p>
</li>
<li>
<p>Save the edited file as “fzdefault.xml” in the “FileZilla FTP Client” folder. If Windows won’t let you save there because you need admin privileges, just save it on your desktop then drag it into the Client folder manually.</p>
</li>
</ol>
<p>That should be it, open FileZilla and all your sites should be in Site Manager. Whatever sites or settings you change now will sync to your other installs after they are configured this way. Also if you are changing settings I like to set my FileZilla theme to 32×32 OpenCrystal, gives it a nice look.</p>Meebo Ruined My Birthday2010-02-16T12:16:00-08:002010-02-16T12:16:00-08:00Justin Montgomerytag:justinmontgomery.com,2010-02-16:/meebo-ruined-my-birthday<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>A few days ago I hear <a href="http://www.meebo.com/">Meebo</a> will finally be releasing an iPhone app on Tues Feb 16th. A red letter date in history, to nobody! So being giddy with anticipation, as existing iPhone IM apps are universally terrible, I download the app last night. First thing I try to do is add my main Google Talk account, rather a Google Apps account, to Meebo. Meebo no likey. It is sometime after midnight and I'm in bed, but I can see a bout of Googling is in my future. So I get up, retrieve my laptop and go to work. After 45 minutes the verdict is in, I'm S.O.L.</p>
<p>"Enjoy a dollop of dump on your cake!" - The Meebo Team</p>
<p>The apparent hangup? Meebo hasn't figured out how to properly support Google Apps like say, oh, EVERY OTHER IM APP. Now, Meebo maintains a wiki where they enthusiastically state you can "<a href="http://wiki.meebo.com/doku.php?id=troubleshooting#can_i_log_into_google_talk_using_my_google_apps_domain">absolutely</a>!" use your Google Apps account...if you can modify obscure DNS SRV records with your hosting company. That is an Oprah sized qualifier there. Many massive domain hosting companies, such as 1and1 and Yahoo, do not let you modify SRV records. I ran across a this <a href="http://help.outlook.com/en-us/140/cc188658.aspx#CompareHostingServices">small list</a> of hosts that do/don't allow SRV modding. Not that it should even be necessary, as I mentioned EVERY OTHER IM APP supports Google Apps!</p>
<p>Well what does Meebo say about proper Apps support? Er, well nothing. <a href="http://forum.meebo.com/viewtopic.php?p=96466&sid=eb8424bcb592590a919349c06e6af20d#p96466">This post</a> from 2 1/2 years does not give me warm feelings that they will modernize their systems anytime soon. Additionally they shutdown registrations on their <a href="http://forum.meebo.com/index.php">forum</a> so I couldn't ask about progress. I decided to ask 1and1, my domain host, if SRV updating was possible, but they replied it was only available for dedicated server accounts and had no plans to add it for the plebs.</p>
<p>So here I am taking it coming from Meebo and going from 1and1, still shackled desktop chat. Ironically, <a href="http://www.pidgin.im/">Pidgin</a> is the only thing that didn't shit on me!</p>My Hand Was Forced2010-02-16T11:30:00-08:002010-02-16T11:30:00-08:00Justin Montgomerytag:justinmontgomery.com,2010-02-16:/my-hand-was-forced<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This post is heinously out of date but I'm keeping it around for historical purposes anyway</p>
</div>
<p>Do I love or hate computers for bringing me to this? The question is probably irrelevant as I and they are forever inextricably entwined. Of course, I will never make peace with that fact. And on this terrible foundation why would I create a blog, a medium so thoroughly disrespected it doesn't even make a good punchline anymore? Because I'm out of touch! I just made a goddamn blog for Christ sake, try to keep up. However, in addition to being out of touch I also spend a lot of time figuring out computer shit. Are the two correlated? Would an autopsy reveal that?</p>
<p>Now this aforementioned "computer shit." How do we "computer people" fix "computer shit?" Google. We generally <a href="http://xkcd.com/627/">don't have a solution</a> to a problem, but we can spend a little/lot/ridiculous/what-does-my-life-mean amount of the day Googling for one! Then, after some painful reflecting time, I need a dumpster in which to place what I have learned about said problem. <strong>Hello dumpster!</strong></p>
<p>So in the name of meager goals, the hope is some of the crap I splatter across these walls will be indexed by the great search engines of the world. Then perhaps it will save people of my ilk a few minutes while doing their own Googling (painful reflecting) and somehow, someone gets a Ferrari.</p>